多线程同步
- 概述
- 信号量
- 示例一
- 代码实现
- 运行结果
- 分析
- 示例二
- 开发环境
- 代码实现
- 运行结果
- 分析
- future和promise
- 示例一
- 实现代码
- 运行结果
- 分析
- 示例二
- 实现代码
- 运行结果
- 示例三
- 实现代码
- 运行结果
- 示例四
- 示例代码
- 运行结果
- 注意
- 附加
- 示例扩展
- 实现代码
- 运行结果
- 注意
- 扩展
- 原子
- 应用场景
- 头文件
- 示例
- 实现代码
- 运行结果
- 分析
概述
本位承接上一篇多线程同步(上),接着讲解剩下的C++多线程同步的三种方式:信号量、future和promise,原子操作。
信号量
在C++中,信号量(semaphore)是一种同步机制,用于控制多个线程对共享资源的访问。信号量通常用于限制对共享资源的并发访问数量,或者用于实现线程之间的同步。
信号量的工作原理类似于一个计数器,它维护着一个整数值。当一个线程需要访问共享资源时,它会尝试获取信号量。如果信号量的值大于零,那么线程将信号量的值减一,并继续执行。如果信号量的值为零,那么线程将被阻塞,直到信号量的值大于零为止。
当一个线程完成对共享资源的访问后,它会释放信号量,即将信号量的值加一。这允许其他等待的线程获取信号量并访问共享资源。
示例一
因为C++11及其标准库中并没有提供原生的信号量类。但C++20特性中提供了信号量类。
在C++20之前,C++标准库没有提供原生的信号量类。相反,开发者通常使用std::condition_variable和std::mutex来模拟信号量的行为,或者依赖第三方库或平台特定的实现来获取信号量的功能。
代码实现
信号量实际上应用计数的原理,用于控制对共享资源的并发访问数量。它允许一定数量的线程同时访问资源,当达到这个数量时,其他尝试获取资源的线程将会被阻塞,直到有线程释放了资源。
需设置可供访问共享资源的最大线程数量,每有一个线程访问,信号量就减一,访问结束后加一。在这个过程中若是信号量的个数为0,则要访问的线程会被阻塞,等待其它正在访问的线程结束后,信号量加一,信号量不为0时才可访问。
下面是使用条件变量和互斥锁实现的信号量。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>using namespace std;const int g_n = 3;
mutex g_mutex;
condition_variable g_cond;
int g_count = 0;void increase() {for (int i = 0; i < 5; ++i) {unique_lock<mutex> lock(g_mutex);g_cond.wait(lock, [] {return g_count < g_n; });++g_count;cout << "increase" << g_count << endl;g_cond.notify_all();}
}void decrease() {for (int i = 0; i < 5; ++i) {unique_lock<mutex> lock(g_mutex);g_cond.wait(lock, [] {return g_count > 0; });cout << "decrease:" << g_count << endl;--g_count;g_cond.notify_all();}
}int main()
{thread t1(decrease);thread t2(increase);t1.join();t2.join();cout << "主线程的id:" << this_thread::get_id() << endl;return 0;
}
运行结果
分析
这里使用互斥锁和条件变量来模拟信号量。加计数中当g_count > g_n,即g_count 的数值大于3的时候阻塞等待,释放锁。加计数线程处理函数释放锁之后,减计数线程处理函数开始获取锁,当g_count > 0,即g_count的值大于0,继续向下执行。相反g_count 的值小于等于0则减计数线程处理函数阻塞等待,释放锁。
其中有一个环节就是当其中的一个线程处理函数被阻塞等待,另一个线程处理函数获得锁之后执行到最后会调用notify_all()来唤醒阻塞等待的线程,但这时候被唤醒的线程能不能得到锁就取决与线程,操作系统的调度策略。
下面详细分析下运行结果下的程序运行过程。
- t1线程启动减计数线程处理函数,获取锁,执行到第二行wait时,由于不满足g_count 大于0的条件(g_count 此时为0),阻塞等待,并释放锁。
- t2线程的加计数线程处理函数获取锁,满足wait条件:g_count 小于3。不用等待继续向下执行,增加g_count 的值并输出。最后唤醒被阻塞的减计数wait,并在i=0这个循环结束为开其i=1的短暂瞬间释放锁。
- 减计数wait被唤醒,但是没有抢到锁。只能继续等待。
- 加计数函数继续i=1的循环,创建unique_lock获取锁,条件满足,继续向下执行。结束i=2的循环前唤醒减计数的wait,当结束本次循环,未在下次循环创建unique_lock获取锁之前,减计数函数都可以去抢锁。但实际上没有抢到锁。只能继续等待。
- 当i =3时,加计数的wait中条件不满足(false),阻塞等待,释放锁。
- 减计数的wait获得锁(因为之前已经被唤醒,所以减计数线程处理函数处于就绪状态,一旦有锁被释放,就会尝试获取锁),满足g_count 大于0,向下继续执行,每个循环中g_count 每次减1,当条件不再满足,就再次释放锁,阻塞等待。
- 此时加计数因为之前已被减计数唤醒,处于就绪状态,减计数一旦释放锁,加计数就会获得锁,满足条件,向下执行。唤醒减计数,但减计数没有获得锁。直到加计数的循环次数达到5次,结束循环,释放锁。
- 减计数获得锁,满足条件,向下执行。直到for循环结束。
示例二
这个示例需要支持c++20,使用semaphore类。
开发环境
vs2019,且将项目属性设置为支持c++20。
代码实现
本例比价简单,主要是理解其如何使用,及其思想。
下面为代码:
#include <iostream>
#include <thread>
#include <semaphore>//C++20
#include <mutex>using namespace std;counting_semaphore g_sem(3);
mutex g_mutex;
#define NUM 5void fun(int i) {g_sem.acquire();g_mutex.lock();//为了输出的线程不被其它访问的线程中断cout << "子线程" <<this_thread::get_id()<<"编号"<<i<< endl;g_mutex.unlock();this_thread::sleep_for(chrono::milliseconds(10));//为了模拟操作耗时g_sem.release();
}
int main()
{thread threads[NUM];for