- 线程及进程关系
- 线程同步与线程安全
- 用atomic/mutex实现线程安全
- join和detach概念
总结了这段时间看的线程和进程相关内容,并且用C++相关库做了对应实现。
才疏学浅,理解不到位的地方,欢迎指正。
不同OS下的多线程方案
C/C++的多线程编程在啊不同操作系统下有不同函数库可以进行调用,在Linux平台下,POSIX多线程库是
线程相关的一些概念的说明
进程与线程的关系
进程是程序分配资源的最小单位,而线程是程序执行的最小单位(实际CPU时间就是分配给各线程的),简要来说,一个进程中可以有多个线程,而这些线程会共用进程中的资源(包括打开的文件、信号标识及动态分配的内存等)和地址空间,而每个执行的线程还会有自己的堆栈区和CPU寄存器状态。
线程的执行顺序和是否可执行是由系统的调度程序决定的,线程有优先级别,优先级别高的程序优先执行(但是同时也要维护进程执行时间分配的公平性)。
在多处理器的机器上,调度程序可以将多个线程放到不同的处理器上运行,这样可以使处理器任务平衡,并提高系统的运行效率。
在进程存在的基础上,我们还需要有线程存在的原因是:
- 进程之间的通信(IPC方式包括有名/匿名管道、信号、信号量、消息队列、共享内存、套接字等)并不算方便,而进程之间因为是共享线程的资源(包括全局/静态变量)因此通信会比较方便,只是在多个线程需要访问(写)同一个变量时,需要注意线程同步(在查找值和改写值之间不中断)的问题。
- 进程的开启和销毁的代价是远大于线程的,当然线程的开启和销毁也是有代价的,因此有线程池的出现(所谓线程池就是将开辟多个线程,维护一个线程池,每次需要新线程时,将从中选取空闲线程,线程用完之后再放回线程池)。
线程的同步与线程安全
线程同步是同一时刻对公共变量的值的认知需要保持一致(不会线程1以为公共变量是1,而线程2以为公共变量是2),如果确保在多线程编程环境中,线程能够正确地处理多个线程之间的共享变量,就称为线程安全。
线程之间共享的变量通常需要注意线程安全的问题,即保证多个线程之间访问同一个变量,不会出现一个线程对公共变量的读写被其他线程打乱的情形(此时需要保证该线程对关键区的访问为原子操作,禁止中断)。
因此通常需要加锁(加锁对象是需要访问公共变量的代码段,我们叫做临界区critical section)也就是常见的mutex(mutual exclusion),保证一个线程对公共变量进行读写操作之间不会被其他线程影响,等到Unlock mutex时变量才可以被其他线程访问。
举个在多线程环境中公共变量访问出错的例子
- 输出说明:预期结果是2e6,但是实际结果明显小于预期结果
- 产生原因:实际
++n
的执行过程仍然是n=n+1
,这涉及了两步操作,一是读取n
,计算n+1
;二是将n+1
的结果赋值给n
。多线程在这里的影响就是一个线程在n+1
的结果赋值给n
之前,另一个线程在原来n
的基础上做+1的计算,而不是在+1之后的结果上做+1,因此最终的结果会小于预期。
1 | //线程不安全例子 |
C++中的解决办法
具体在C++中实现时:
- 为了避免编译器对变量做不必要的优化,通常需要将公共变量声明为易失性变量volatile,使得线程每次访问该变量时需要从内存中读取即时值,而不能从线程自己的寄存器中读取暂存值,以保证线程间的同步。关于volatile是为多线程准备的,这点是有争议的(因此这点暂放)。
- 对访问公共变量的临界区代码我们可以使用mutex锁,保证在mutex的lock加锁和unlock之间执行临界区代码时不产生中断。
按照mutex加锁,我们可以得到方案1:mutex锁保证线程安全解决上述例子中的问题。
1 | //方案1:mutex锁保证线程安全 |
- 对于某些简单的操作,使用mutex来保证线程安全会比较昂贵,因此可以使用原子变量
(对于原子对象,即如果一个线程对原子对象进行写,另一个线程对对象进行读时,行为是明确的;另外一方面,当获取原子变量的值时会确立线程同步)来提供一个可使函数既线程安全又可重入(概念解释如下)的方案 - 编译器在编译阶段通常会对我们的代码进行优化(如乱序执行、指令重排等)以追求更高的效率,而这种操作在多线程环境下,通常会引发很多问题。而定义的原子操作就涉及对这些方面的说明,包含我们三个方面关心的语义:1. 操作本身是不可分割的(Atomicity),一个线程对某个数据的访问何时对另外一个线程可见(visibility),执行的顺序是否可以重排(Ordering)。
- 可重入的程序是,在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又执行了该程序,但却不出错。可重入的函数应当满足,所有变量都保存在调用栈的当前函数栈/帧(frame)上,因此同一执行线程重入执行该函数时加载了新的函数帧,与前一次执行函数使用的函数帧不冲突。
- 函数帧:又叫栈帧,每一次函数调用都会产生一个函数帧,每个栈帧对应一个未运行完的函数,栈帧中保存了该函数的返回地址和局部变量,记录着函数的执行环境。
运用原子变量,我们就得到了方案2:用原子变量来保证线程安全:
1 | //方案2:用原子变量来保证线程安全 |
join
及detach
上述例子中已经用到了join
和detach
,以下对两者进行说明。
对于C++程序而言,你的main函数就是你的主线程,主线程在任何需要的时候都可以创建新的线程,当线程执行完毕时,自动终止线程;当进程结束时,所有线程都必须终止。
- 所谓
join
就是主线程在调用join
方法的位置等待子线程会合,会合之后执行下一步操作。 - 所谓
detach
就是,主线程将不等待子线程的会合,自己直接结束生命,子线程未执行完也不报错,会自己在执行完毕后会退出。
如果某个线程,不声明以上两者之一,但是在主线程结束之前又没有执行完毕,程序执行时会崩溃。
参考资料
- 参考某多线程文章
- wikipedia