c++多线程——信号量
1.前言
信号量(semaphore)用于多个线程间的通讯,解决临界资源的互斥访问和同步控制问题。信号量就类似于一个计数器(非负的),这个计数器记录了资源的数量,如果大于0,就可以取用资源。PV操作
-
P操作是表达取用一个资源。当线程执行了P操作,如果信号量等于0,则会使线程阻塞,直到信号量转变成大于0,再使信号量减1。如果大于0,则会直接使信号量减1
-
V操作是表示释放一个资源。当线程执行了V操作,会使信号量加1,唤醒一个因为P操作阻塞的线程。
以下代码所需要的头文件
//使用c++20规则
//(semaphore)在20才出现
#include<iostream>
#include<thread>
#include<semaphore>
#include<chrono>
2.信号量(std::counting_semaphore)的使用
2.1 std::counting_semaphore 的基础
c++ 中使用 std::counting_semaphore
来表示信号量,用于同一进程间的线程同步,std::counting_semaphore
是不可移动,不可复制的。
他与条件变量不同的是,信号量可以不需要互斥锁也可以实现,相比于等待一个条件满足,对信号量的加减都会被记录,不需要像条件变量一样保证条件检查和等待操作的原子性。
以下语句展示了信号量 signal
的初始化。
std::counting_semaphore<2> signal(1);
可以看到在初始化的语句中出现了两个数字,尖括号(<2>)里的数字表示信号量的最大值为 2。后面圆括号((1))表示信号量的初始值为 1.
2.2 std::counting_semaphore 的函数
接下来介绍信号量操作中要遇到的几个函数,负责实现PV操作等功能。
- void release(ptrdiff_t _Update = 1),信号加 1 操作,对应V操作。这个信号加 1 操作被设定为原子操作,完成加 1 后如果线程等待的个数小于信号量的值,之间唤醒所有线程,否则一个一个唤醒。
可以看到他信号量默认的增加值 _Update
为 1 。但是我们可以创建一个 ptrdiff_t
变量传入来修改每次调用 release 函数的增加值.
ptrdiff_t _Semaphore_add = 2;
std::counting_semaphore<2> ready(0);
ready.release(_Semaphore_add); //v操作
注意!!,信号量的最大值要大于信号量的增加值。
-
void acquire(),信号量减 1 操作,对应P操作。同样的这个信号减 1 操作也被设定为原子操作。如果信号量等于 0 ,他会进入阻塞等待,直到被唤醒,他会再判断信号量是否大于 0 。
-
bool try_acquire(),信号量减 1 操作,不过是尝试去减 1 ,也就是非阻塞操作。
-
bool try_acquire_for(const chrono::duration<_Rep, _Period>& _Rel_time),信号量减 1 操作,这里要传入一个时间范围。
如果在时间范围内获取资源(信号量减 1 操作)成功则返回 true
,否则则会超时返回 false
.
- bool try_acquire_until(const chrono::time_point<_Clock, _Duration>& _Abs_time),信号量减 1 操作,这里要传入一个时间点。
如果在时间点到达前获取资源(信号量减 1 操作)成功则返回 true
,否则则会超时返回 false
.
下面是一些代码实现上面的函数
std::counting_semaphore<1> ready(0);
std::counting_semaphore<1> done(1);
void prepare() {
done.acquire(); //p操作,确保prepare在work执行完成前不再执行
std::cout << "Preparing...\n";
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Preparation is done\n";
ready.release(); //v操作,将ready信号量加1并唤醒work线程
}
void work() {
ready.acquire(); //等待ready信号量
std::cout << "Working...\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Work is done\n";
done.release(); //v操作,将done信号量加1并唤醒prepare线程
}
可以看到这里定义了两个信号量 ready
(表示准备好的资源个数) 和 done
(表示要生产资源的任务个数)。设置信号量最大值为 1 ,ready
的初始值为 0 ,done
的初始值为 1 。
然后是两个函数 prepare()
(生产者),work()
(消费者)。prepare()
模拟了负责准备资源,先对 done
信号量减 1 然后使 ready
信号量加 1 。work()
模拟了负责消费资源,先对 ready
信号量减 1 然后使 done
信号量加 ```1 。
主函数
点击查看代码
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back(prepare);
}
for (int i = 0; i < 3; ++i) {
threads.emplace_back(work);
}
for (auto& t : threads) {
t.join(); //等待所有线程完成
}
return 0;
}
得到的结果类似
Preparing...
Preparation is done
Working...
Work is done
Preparing...
Preparation is done
Working...
Work is done
Preparing...
Preparation is done
Working...
Work is done
下面是另一种实现方式
std::counting_semaphore<3> signal(2);
void worker(int id) {
while(!signal.try_acquire_for(std::chrono::seconds(1))) { //p操作,如果信号量大于0,则将其减1
std::cout << "." ;
}
printf("Worker %d is working\n", id);
std::this_thread::sleep_for(std::chrono::seconds(4));
printf("Worker %d is done\n" ,id);
signal.release(); //v操作,将信号量加1并唤醒阻塞的线程
}
主函数
点击查看代码
int main() {
std::vector<std::thread> workers;
for (int i = 0; i < 5; ++i) {
workers.emplace_back(worker, i);
}
for (auto& worker : workers) {
worker.join();
}
return 0;
}
得到的结果类似
Worker 0 is working
Worker 1 is working
.........Worker 1 is done
Worker 0 is done
Worker 2 is working
Worker 3 is working
....Worker 2 is done
Worker 3 is done
Worker 4 is working
Worker 4 is done
3.信号量(std::binary_semaphore)的使用
3.1 std::binary_semaphore 的基础
std::binary_semaphore
是二进制信号量,最大计数为 1,只有两个值,0 和 1 。
std::binary_semaphore signal(1); //二进制信号量,初始值为1
可以看到他的最大值固定为 1 ,所以无需设置最大值。而后面圆括号((1)),表示把信号量的初始值设置为 1 .他只能被设置为 0 或 1。
std::binary_semaphore
信号量确保了在任何时刻只有一个线程可以使用资源。因为只有两种状态,所以使用的系统资源也相较于之前的std::counting_semaphore
少
3.2 std::binary_semaphore 的函数
std::binary_semaphore
所拥有的函数和上面std::counting_semaphore
相同这里仅展示一下用法。
std::binary_semaphore signal(1); //二进制信号量,初始值为1
void worker(int id) {
signal.acquire();
printf("Worker %d is working\n", id);
std::this_thread::sleep_for(std::chrono::seconds(2));
printf("Worker %d is done\n" ,id);
signal.release();
}
以上为我在学习过程中整理的知识点,如有哪里说错了感谢指出
推荐一个讲的比较好的up主的视频【42.信号量 和 counting_semaphore】https://www.bilibili.com/video/BV1V2421Z7ZS?vd_source=dccc0abff62c8559f0a5ed0bce39dec2