在线程池中存在两个队列,一个任务队列用来缓存任务,一个线程队列用来缓存线程。在多线程环境下,需要考虑两个队列的线程同步问题。
线程互斥
要判断一段代码是否能够被多线程执行,要看这段代码中是否存在竞态条件。
竞态条件(Race Condition)是指在并发环境中,当有多个线程或进程同时访问同一个临界资源时,由于多个线程的并发执行顺序的不确定,从而导致程序输出结果的不确定。这种情况发生在计算的正确性取决于多个线程的交替执行时序时。竞态条件可能导致程序运行顺序的改变,进而影响最终结果,产生超出预期的情况。
如果该代码片段存在竞态条件,那么则称这段代码区域为临界区,临界区是不能被并发访问且不可重入的,需要保证它的原子操作。
如果该代码片段不存在竞态条件,那么则称这段代码区域是可重入的。
想要保证临界区的原子操作,可以使用互斥锁mutex或者原子类型atomic。
在进入临界区之前获取互斥锁、在离开临界区释放互斥锁,就能够保证在同一时间只有持有互斥锁的一个线程能够进入临界区执行代码而其他申请锁的线程只能挂起等待锁的释放,这样就能够保证临界区的原子操作。如果临界区不大,获取锁的速度非常快,也可以使用乐观锁来代替悲观锁,此时当持有锁的线程正在临界区执行代码,而其余线程在申请锁时就不会被挂起,而是一直申请锁,这样就不会让其余线程刚被阻塞挂起没多久就因锁被释放而唤醒。
除了互斥锁,C++11还提供了CAS操作(无锁机制) atomic。无锁机制并不是不使用锁,而是使用一种基于活锁CAS(Compare And Swap)操作来实现。
atomic可以使变量的++或--操作变成原子操作,那么如果临界区内只是对某个变量进行++或--操作,我们就可以将该变量变成atomic原子类型,这样就可以避免申请和释放锁的开销了。
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
using namespace std;
int main() {
int i = 0;
atomic<int> j = 0;
vector<thread> threads;
threads.emplace_back([&i](int n)->void {while (n--) { ++i; } }, 100000);
threads.emplace_back([&i](int n)->void {while (n--) { ++i; } }, 100000);
threads.emplace_back([&j](int n)->void {while (n--) { ++j; } }, 100000);
threads.emplace_back([&j](int n)->void {while (n--) { ++j; } }, 100000);
for (auto& thread : threads) {
thread.join();
}
cout << "i = " << i << " " << "j = " << j << endl;
return 0;
}
线程通信
现有线程1、线程2两个执行流,线程1执行的任务1依赖于线程2执行的任务2得到的结果,虽然图上任务2的执行顺序在任务1之前,但是由于线程的调度完全是由系统内核的调度算法决定的,所以CPU先执行哪个任务是不确定的,为了保证任务2在任务1之前被执行,就需要线程之间进行通信,如果任务1先被调度执行就进入等待状态,等任务2执行完毕了再来继续执行。为了完成线程间通信,我们可以借助条件变量condition_variable和信号量semaphore。
谈到线程间通信我们就需要想到多线程模型中的生产者消费者模型。 生产者消费者模型可以被称为“三二一原则”,
三指的是三种关系:
① 生产者与生产者之间的互斥关系;
② 生产者与消费者之间的互斥且同步关系;
③ 消费者与消费者之间的互斥关系。
二指的是两种角色:生产者和消费者。
一指的是一个交易场所:缓冲区。
生产者消费者模型可以让生产者和消费者进行解耦,也支持并发和忙闲不均。
借助条件变量和互斥锁(条件变量必须要和互斥锁结合起来使用,这也使条件变量的控制精细程度比信号量要好),我们可以来实现生产者消费者模型:
详细可以参考:
信号量是一种用于控制对共享资源的访问的机制。它可以用来限制同时访问某个资源的线程数量。在上文中我们谈到一把互斥锁最多只能被一个线程所获取,那么我们就可以把互斥锁看作是一个资源计数只能是0或1的资源。那为什么现在要说这个呢?是因为我们可以把信号量看作一个资源计数没有限制的互斥锁,当信号量只在0和1之间变动时(互斥量、二元信号量),就可以把信号量看作成一把轻量的互斥锁。但是互斥量实现的互斥锁还是与真正的互斥锁有所区别的,互斥锁只能是哪个线程获取的锁,哪个线程释放;而互斥量可以由不同的线程来acquire和release。
信号量一般不依赖于互斥锁,它可以独自使用,如本文线程通信的导入问题,让任务2先于任务1执行就可以用信号量来解决。但是信号量也可以与互斥锁一起使用来实现生产者消费者模型:
注: 在使用信号量时,一定要先申请信号量再申请互斥锁。因为互斥锁只能由一个线程持有,如果先申请互斥锁的话,那么除了持有互斥锁的线程,其余的线程就会被阻塞在互斥锁处,而去申请信号量,那么信号量就只会被持有锁的线程申请和释放,信号量就形同虚设了。
详情可以参考:
总结起来,信号量主要用于控制对共享资源的访问,而条件变量主要用于线程间的通信和协作。它们在不同的场景下有不同的作用和用途。