本篇文章围绕以下几个问题展开:
c++的多线程可以充分利用计算机资源,提高代码运行效率。在这里总结了一些多线程应用过程中的基本概念和用法。
引入线程之后,将传统进程的两个基本属性分开了,线程作为调度和分配的基本单位,进程作为独立分配资源的单位。
我对这句话的理解是:线程参与操作系统的调度,参与CPU的竞争,得到分配的时间片,获得处理机(CPU)运行。而进程负责获取操作系统分配的资源,如内存。
简单来说,并发指的是两个或多个独立的活动在同一时段内发生
与并发相近的另一个概念是并行。它们两者存在很大的差别,图示如下:
在传统的单核CPU中,CPU通过极快的速度不停的切换不同应用程序的命令,而让我们看起来感觉计算机在同时执行很多个应用程序。比如,一边听歌,一边聊天,还能同时打游戏,我们误以为这是并发,其实只是一种伪并发的假象。
在出现多核处理器以后,使得并发真正的实现。C++中采用多线程实现并发。
一个多线程C++程序是什么样子的?它看上去和其他所有C++程序一样,通常是变量、类以及函数的组合。唯一真正的区别在于某些函数可以并发运行,所以你需要确保共享数据的并发访问是安全的。当然,为了并发地运行函数,必须使用特定的函数以及对象来管理各个线程。
C++11新标准多线程支持库
C++标准并没有提供对多进程并发的原生支持,所以C++的多进程并发要靠其他API(比如用CreateThread进行创建)
C++11可以通过多线程实现并发,这是一种比较底层、传统的实现方式。C++11引入了5个头文件来支持多线程编程,是<atomic>/<thread>/<mutex>/<condition_variable>/<future>
std::thread类成员函数:
有时候我们需要在线程执行代码里面对当前调用者线程进行操作,针对这种情况,C++11里面专门定义了一个命名空间this_thread,此命名空间也声明在<thread>头文件中,其中包括:
std::thread的关键总结:
容易知道,如下几种情况下,std::thread对象是不关联任何线程的(对这种对象调用join或detach接口会抛异常):
默认构造的thread对象;
被移动后的thread对象;
detach 或 join 后的thread对象;
C++中多线程创建:
1️⃣简单使用
#include <iostream> #include <thread> using namespace std; void f() { cout<<"thread 1 is running"<<endl; this_thread::sleep_for(chrono::seconds(1)); } int main() { thread t1(f); //创建线程,一旦创建完毕,马上开始运行。 t1.join(); }
这里创建传入的函数f,实际上其构造函数需要的是可调用(callable)类型,只要是有函数调用类型的实例都是可以的。所有除了传递函数外,还可以使用:
for (int i = 0; i < 4; i++) { thread t([i]{ cout << i << endl; }); t.detach(); }
#include <iostream> #include <thread> using namespace std; class Task { public : void operator()(int i) //()重载 { cout << i << endl; } }; int main() { for (int i = 0; i < 4; i++) { Task task; thread t(task, i); t.detach(); } }
当线程启动后,一定要在和线程相关联的thread
销毁前,确定以何种方式等待线程执行结束。C++11有两种方式来等待线程结束
这就涉及到多线程编程最核心的问题了资源竞争。CPU有4核,可以同时执行4个线程是没有问题的。但是控制台(资源)却只有一个,同时只能有一个线程拥有这个唯一的控制台,将数字输出。
2️⃣带函数参数的线程
当需要向线程函数传递参数时,直接在创建线程时,同时也把参数作为入参传递给线程函数。
注意当调用函数的参数为引用参数时,线程调用需要加上ref关键字表示引用。并且线程函数会改变引用的变量值。
void f1(int n) { n++; cout<<"n = "<< n <<endl; } void f2(int &n)//引用参数 { n++; cout<<"n = "<<n<<endl; } int main() { int n = 0; thread t1(f1, n); t1.join(); cout<<"n = "<<n<<endl; thread t2(f2, ref(n)); t2.join(); cout<<"n = "<<n<<endl; }
运行结果为:
3️⃣转移线程的所有权
thread
是可移动的(movable)的,但不可复制(copyable)。可以通过move
来改变线程的所有权,灵活的决定线程在什么时候join或者detach。
void f2(int &n) { n++; cout<<"n = "<<n<<endl; } int main() { int n = 0; thread t3(f2, ref(n)); thread t4(move(t3)); //此时t4正在运行f2(),t3不再是一个线程了。 t4.join(); }
运行结果:
将线程从t3转移给t4,这时候t3就不再拥有线程的所有权,调用t3.join
或t3.detach
会出现异常,要使用t4来管理线程。这也就意味着thread
可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程。
4️⃣线程暂停
如果让线程从外部暂停会引发很多并发问题,这也是为什么std::thread没有直接提供pause函数的原因。
如果线程在运行过程中,确实需要停顿,就可以用this_thread::sleep_for。
this_thread::sleep_for(chrono::seconds(3)); //此处线程停顿3秒。
5️⃣获取当前线程号
线程的标识类型为std::thread::id
,有两种方式获得到线程的id。
thread
的实例调用get_id()
直接获取this_thread::get_id()
获取thread::id main_threadId = this_thread::get_id();
小结:
本结主要介绍了C++11引入的标准多线程库的一些基本操作。有以下内容:
std::ref
进行转换get_id
获取。线程管理的示例代码
创建线程,并观察线程的并发执行与阻塞等待 #include <iostream> #include <thread> #include <chrono> using namespace std; void thread_function(int n) { std::thread::id this_id = std::this_thread::get_id(); //获取线程ID for (int i = 0; i < 5; i++) { cout << "子线程 " << this_id << " 运行 : " << i + 1 << endl; std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒 } } class Thread_functor { public: // functor行为类似函数,C++中的仿函数是通过在类中重载()运算符实现,使你可以像使用函数一样来创建类的对象 void operator()(int n) { std::thread::id this_id = std::this_thread::get_id(); for (int i = 0; i < 5; i++) { cout << "子仿函数线程" << this_id << " 运行: " << i + 1 << endl; std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒 } } }; int main() { thread mythread1(thread_function, 1); // 传递初始函数作为线程的参数 if (mythread1.joinable()) //判断是否可以成功使用join()或者detach(),返回true则可以,返回false则不可以 mythread1.join(); // 使用join()函数阻塞主线程直至子线程执行完毕 Thread_functor thread_functor; //函数对象实例化一个对象 thread mythread2(thread_functor, 3); // 传递初始函数作为线程的参数 if (mythread2.joinable()) mythread2.detach(); // 使用detach()函数让子线程和主线程并行运行,主线程也不再等待子线程 auto thread_lambda = [](int n) { //lambda表达式格式:[捕获列表](参数列表)可变异常->返回类型{函数体} std::thread::id this_id = std::this_thread::get_id(); for (int i = 0; i < 5; i++) { cout << "子lambda线程" << this_id << " 运行: " << i + 1 << endl; std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒 } }; thread mythread3(thread_lambda, 4); // 传递初始函数作为线程的参数 if (mythread3.joinable()) mythread3.join(); // 使用join()函数阻塞主线程直至子线程执行完毕 std::thread::id this_id = std::this_thread::get_id(); for (int i = 0; i < 5; i++) { cout << "主线程" << this_id << " 运行: " << i + 1 << endl; std::this_thread::sleep_for(std::chrono::seconds(1)); } getchar(); return 0; }
运行结果:
上面的代码分别用三种函数对象创建了三个线程,其中第一个线程mythread1阻塞等待其执行完后继续往下执行,第二个线程mythread2不阻塞等待在后台与后面的第三个线程mythread3并发执行,第三个线程继续阻塞等待其完成后再继续往下执行主线程任务。为了便于观察并发过程,对三个线程均用了睡眠延时this_thread::sleep_for(duration)函数,且延时时间作为参数传递给该函数。
上面的示例假如多重复运行几次,有很大可能会出现某行与其他行交叠错乱的情况(如下图所示),为何会出现这种情况呢?
这就涉及到多线程资源竞争的问题了,即一个线程对某一资源(这里指显示终端)的访问还未完成,另一线程抢夺并访问了该资源,导致该资源数据混乱情况的出现。
解决方案详见下一篇文章:
相关博文:(2条消息) C++多线程并发(一)--- 线程创建与管理_流云-CSDN博客_c++多线程并发