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

posted @ 2025-04-23 18:32  散尽天华  阅读(383)  评论(0)    收藏  举报