目录
C++的多线程可以充分利用计算机资源,提高代码运行效率。在这里总结了一些多线程应用过程中的基本概念和用法。
进程和线程的区别
- 进程是一个应用程序被操作系统拉起来加载到内存之后从开始执行到执行结束的这样一个过程。简单来说,进程是程序(应用程序,可执行文件)的一次执行。比如双击打开一个桌面应用软件就是开启了一个进程。
- 线程是进程中的一个实体,是被系统独立分配和调度的基本单位。也有说,线程是CPU可执行调度的最小单位。也就是说,进程本身并不能获取CPU时间,只有它的线程才可以。
引入线程之后,将传统进程的两个基本属性分开了,线程作为调度和分配的基本单位,进程作为独立分配资源的单位。
我对这句话的理解是:线程参与操作系统的调度,参与CPU的竞争,得到分配的时间片,获得处理机(CPU)运行。而进程负责获取操作系统分配的资源,如内存。
何为并发?
简单来说,并发指的是两个或多个独立的活动在同一时段内发生
与并发相近的另一个概念是并行。它们两者存在很大的差别,图示如下:
-
并发:同一时间段内可以交替处理多个操作,强调同一时段内交替发生。
-
并行:同一时刻内同时处理多个操作,强调同一时刻点同时发生。
在传统的单核CPU中,CPU通过极快的速度不停的切换不同应用程序的命令,而让我们看起来感觉计算机在同时执行很多个应用程序。比如,一边听歌,一边聊天,还能同时打游戏,我们误以为这是并发,其实只是一种伪并发的假象。
在出现多核处理器以后,使得并发真正的实现。C++中采用多线程实现并发。
C++11线程基本操作
一个多线程C++程序是什么样子的?它看上去和其他所有C++程序一样,通常是变量、类以及函数的组合。唯一真正的区别在于某些函数可以并发运行,所以你需要确保共享数据的并发访问是安全的。当然,为了并发地运行函数,必须使用特定的函数以及对象来管理各个线程。
C++11新标准多线程支持库
C++标准并没有提供对多进程并发的原生支持,所以C++的多进程并发要靠其他API(比如用CreateThread
进行创建)
C++11可以通过多线程实现并发,这是一种比较底层、传统的实现方式。C++11引入了5个头文件来支持多线程编程,是<atomic>
、<thread>
、<mutex>
、<condition_variable>
、<future>
- <
thread
> : 提供线程创建及管理的函数或类接口; - <
mutex
> : C++11 互斥量Mutex。在多线程环境中,有多个线程竞争同一个公共资源,就很容易引发线程安全的问题 - <
condition_variable
> : 允许一定量的线程等待(可以定时)被另一线程唤醒,然后再继续执行; - <
future
> : 提供了一些工具来获取异步任务(即在单独的线程中启动的函数)的返回值,并捕捉其所抛出的异常 - <
atomic
>:为细粒度的原子操作(不能被处理器拆分处理的操作)提供组件,允许无锁并发编程。
std::thread类成员函数
get_id
:获取线程ID,返回一个类型为std::thread::id
的对象。joinable
:检查线程是否可被join
。检查thread
对象是否标识一个活动(active
)的可行性线程。缺省构造的thread
对象、已经完成join
的thread
对象、已经detach
的thread
对象都不是joinable
。join
:通过join()
函数关联并阻塞线程,等待该线程执行完毕后继续;detach
:通过detach()
函数解除关联使线程可以与主线程并发执行,但若主线程执行完毕退出后,detach()
接触关联的线程即便没有执行完毕,也将自动退出。native_handle
:该函数返回与std::thread
具体实现相关的线程句柄。swap
:交换两个线程对象所代表的底层句柄。operator
:移动线程对象
有时候我们需要在线程执行代码里面对当前调用者线程进行操作,针对这种情况,C++11里面专门定义了一个命名空间this_thread
,此命名空间也声明在<thread>
头文件中,其中包括:
get_id()
函数用来获取当前调用者线程的ID;yield()
函数可以用来将调用者线程跳出运行状态,重新交给操作系统进行调度,即当前线程放弃执行,操作系统调度另一线程继续执行;sleep_until()
函数是将线程休眠至某个指定的时刻(time point),该线程才被重新唤醒;sleep_for()
函数是将线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠实际可能比sleep_duration所表示的时间片更长。
std::thread的关键总结
- C++ 11中创建线程非常简单,使用
std::thread
类就可以,thread
类定义于thread
头文件,构造thread
对象时传入一个可调用对象作为参数(如果可调用对象有参数,把参数同时传入),这样构造完成后,新的线程马上被创建,同时执行该可调用对象 - 用
std::thread
默认的构造函数构造的对象不关联任何线程;判断一个thread
对象是否关联某个线程,使用joinable()
接口,如果返回true,表明该对象关联着某个线程(即使该线程已经执行结束); "joinable"
的对象析构前,必须调用join()
接口等待线程结束,或者调用detach()
接口解除与线程的关联,否则会抛异常;- 正在执行的线程从关联的对象
detach
后会自主执行直至结束,对应的对象变成不关联任何线程的对象,joinable()
将返回false; std::thread
没有拷贝构造函数和拷贝赋值操作符,因此不支持复制操作(但是可以move
),也就是说,没有两个std::thread
对象会表示同一执行线程;- 容易知道,如下几种情况下,
std::thread
对象是不关联任何线程的(对这种对象调用join
或detach
接口会抛异常):- 默认构造的
thread
对象; - 被移动后的
thread
对象; detach
或join
后的thread
对象;
- 默认构造的
C++中多线程创建
- 简单使用
这里创建传入的函数f,实际上其构造函数需要的是可调用(callable)类型,只要是有函数调用类型的实例都是可以的。所有除了传递函数外,还可以使用:#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(); return 0; }
- lambda表达式
for (int i = 0; i < 4; i++) { thread t([i]{ cout << i << endl; }); t.detach(); }
- 重载了()运算符的类的实例
当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束。C++11有两种方式来等待线程结束#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(); } }
- detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。前面代码所使用的就是这种方式。因此执行的结果就多种多样了。
这就涉及到多线程编程最核心的问题了资源竞争。CPU有4核,可以同时执行4个线程是没有问题的。但是控制台(资源)却只有一个,同时只能有一个线程拥有这个唯一的控制台,将数字输出。 - join方式,等待启动的线程完成,才会继续往下执行。假如前面的代码使用这种方式,其输出就会0,1,2,3,因为每次都是前一个线程输出完成了才会进行下一个循环,启动下一个新线程。
- detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。前面代码所使用的就是这种方式。因此执行的结果就多种多样了。
-
带函数参数的线程
当需要向线程函数传递参数时,直接在创建线程时,同时也把参数作为入参传递给线程函数。
注意当调用函数的参数为引用参数时,线程调用需要加上ref关键字表示引用。并且线程函数会改变引用的变量值。#include <iostream> #include <thread> using namespace std; 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; } /* n = 1 n = 0 n = 1 n = 0 */
-
转移线程的所有权
thread
是可移动的(movable
)的,但不可复制(copyable
)。可以通过move
来改变线程的所有权,灵活的决定线程在什么时候join
或者detach
。#include <iostream> #include <thread> using namespace std; void f2(int &n) { n++; cout << "n = " << n << endl; } int main() { int n = 0; thread t3(f2, ref(n)); thread t4(move(t3)); t4.join(); //此时t4正在运行f2(),t3不再是一个线程了。 return 0; } /* n = 1 */
将线程从t3转移给t4,这时候t3就不再拥有线程的所有权,调用
t3.join
或t3.detach
会出现异常,要使用t4来管理线程。这也就意味着thread
可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程。 -
线程暂停
如果让线程从外部暂停会引发很多并发问题,这也是为什么std::thread
没有直接提供pause
函数的原因。
如果线程在运行过程中,确实需要停顿,就可以用this_thread::sleep_for
。this_thread::sleep_for(chrono::seconds(3)); //此处线程停顿3秒。
-
获取当前线程号
线程的标识类型为std::thread::id
,有两种方式获得到线程的id。- 通过
thread
的实例调用get_id()
直接获取 - 在当前线程上调用
this_thread::get_id()
获取
thread::id main_threadId = this_thread::get_id();
- 通过
小结:
本结主要介绍了C++11引入的标准多线程库的一些基本操作。有以下内容:
- 线程的创建
- 线程的执行方式,
join
或者detach
- 向线程函数传递参数,需要注意的是线程默认是以拷贝的方式传递参数的,当期望传入一个引用时,要使用
std::ref
进行转换 - 线程是
movable
的,可以在函数内部或者外部进行传递 - 线程在运行过程中,如果需要停顿,可以用
this_thread::sleep_for
实现。 - 每个线程都一个标识,可以调用
get_id
获取。