多线程编程
《C++ 11 多线程编程》
1. 基础
进程:一个进程代表计算机中实际运行的一个程序,在现代计算机操作系统的保护模式下,每个进程都具有自己独立的进程地址空间和上下文堆栈;进程并不负责执行进程代码,只是为程序提供一个大环境容器,进程中的实际执行体是线程(Thread),因此在一个进程中至少得有一个线程,这个线程被称为主线程;
线程是进程中实际执行代码的最小单元,具体由操作系统安排调度,决定其合适启动,运行,暂停,消亡;
在Windows系统下,当一个进程存在多个子线程的时候,如果主线程执行结束,这时子线程(支线程)即使还没有执行完相应的代码,也会退出。因此在Windows下设计多线程程序的时候,需要确保在子线程执行完之前,主线程保持不退出;
Linux系统下,主线程退出,不会影响子线程的执行,子线程还会继续运行,但是此时的进程会变成僵尸进程。在Linux下设计多线程程序时,应该避免这种情况;
某个线程奔溃,是否会导致进程退出?
一般来说,每个线程都是一个独立的执行单位,有自己的上下文堆栈,一个线程的奔溃不会对其他的线程造成影响;但是实际情况是,一个线程奔溃,会导致整个进程退出;例如Linux下会产生一个Segment Fault (段错误)信号,操作系统对这个信号的默认处理方式就是结束进程;整个进程都被销毁,则进程中其他线程自然也不会存在了;
2. 等待线程结束
Linux下通过调用pthread_join()函数等待线程结束,pthread_join()函数在等待目标线程退出期间,会挂起当前线程,被挂起的线程出于等待状态,不会消耗CPU时间片;直到目标线程退出之后,调用pthread_join()的线程才会被唤醒,继续执行后续的逻辑。
C++11等待线程结束:c++11的线程std::thread
提供了join()
方法来等待线程结束,使用这个方法时,必须保证线程是处于运行状态,即等待的线程必须是可join的,如果需要等待的线程已经退出,此时调用join方法,会导致程序崩溃;因此c++11线程库还提供了一个joinable()
方法,判断一个线程是否是可以join()的。
3. c++ 11多线程
关于c++11多线程有以前的两篇博客可参考:
c++11之前,并没有对多线程提供语言级别的支持;这使得用c++编写可移植的并发程序时,存在诸多不便;
c++多线程相关知识点:
- 线程同步的互斥量
- 线程通信的条件变量
- 线程安全的原子变量
- call once用法
- 用于异步操作的future, promise, task
- 线程异步操作函数async
4. 整型变量的原子操作
线程同步技术,指的是多个线程同时操作某个资源,这里的资源可能是简单的整型变量,或者是一个复杂的对象;对资源的操作一般是指对资源的读写操作,在操作过程中需要采取一些措施对这些资源进行保护,避免引起资源访问冲突,或者得到意料之外的结果。
4.1 整型变量是不是原子操作
整型变量操作大致有以下三种情况:
- 给整型变量赋值
int a = 110;
这样的操作一般是原子操作,需要一条计算机指令就能完成,即CPU将立即数110搬运到变量a的内存地址中;
- 变量自增或者自减
a++;
a--;
从C++层面,这条语句是一个原子操作,但是从编译器得到的汇编指令来看其实不是,这条语句对应三条指令;1. 首先将变量a对应的值mov到某个寄存器。2. 将寄存器中的值自增1。 3. 将寄存器中自增后的值mov到变量a对应的内存地址;
例如:两个线程同时对变量a进行操作
int a = 0;
// 线程1 操作a自增
thread1()
{
a++;
}
// 线程2 操作a自增
thread2()
{
a++;
}
假设线程1将变量a的值加载到寄存器,对其进行自增操作,此时还未将寄存器中的值mov回变量地址内;由于操作系统调度的不确定性,切换到线程2开始执行,此时将a变量的值(还是0)mov到寄存器中,进行自增操作,寄存器内的值也为1;此时切换线程1继续执行后,a变量的值被置为1,线程2结束后,a变量的值也为1,与预想的值(2)不相等。
- 把一个变量的值赋给另一个变量,或者是把一个表达式的值赋给另一个表达式
例如:
int a = b;
从汇编的角度来看,这条语句也需要多条指令才能完成,由于CPU架构体系的限制,数据不能从内存的一处直接搬运的另一处,必须通过寄存器中转才能完成;此时如果是多线程操作,则会产生不确定性的结果;