「C++ 多线程」信号量 counting_semaphore 与 binary_semaphore

文章大图来源:pixiv_id=114949296

1 信号量

1.1 基本概念

多线程编程中,信号量 是用于控制对公共资源的访问、协调多个线程或进程的一种重要同步工具。它是 一种计数器,用来 限制资源被并发访问的线程数量

1.2 计数信号量

计数信号量(Counting Semaphore)与传统的互斥锁不同,其允许多个线程同时访问相同的资源。

计数信号量 初始值为可用资源的数量,当线程获取资源时,信号量减一;释放资源时,信号量加一。

如果信号量的值达到零,表示没有可用资源,新的线程请求会被阻塞直到资源被释放。

1.3 二进制信号量

二进制信号量(Binary Semaphore)有点类似于传统互斥锁的功能,其值只有 0 和 1。

主要用于实现互斥访问,一个线程获取资源后,其他线程必须等待,直到资源被释放。

2 std::count_semaphore

2.1 基本概念

std::counting_semaphore 于 C++20 引入,位于位于 <semaphore> 头文件中。

std::counting_semaphore 允许多个线程同时访问一个资源。在多线程编程中,它用于限制可同时访问资源的最大线程数量。

std::counting_semaphore 是一个模板类,构造时需要指定最大计数值,即能够同时访问资源的线程数。

2.2 常规操作

  • acquire()

    尝试减少信号量的计数值

    • 如果当前计数值为 0,则线程会被阻塞,直到有其他线程调用 release() 并增加计数值;

    • 如果当前计数值不为 0,则还运行当前计数值数量的线程同时访问共享数据。

  • release()

    增加信号量的计数值。可能会唤醒一个或多个正在等待资源的线程。

2.3 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <semaphore>
#include <thread>
#include <vector>

std::counting_semaphore<2> sem(2); // 最多允许2个线程同时访问

void foo(int id) {
sem.acquire(); // 获取资源权限
std::cout << "Thread " << id << " is accessing the resource\n";
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟资源访问
std::cout << "Thread " << id << " is releasing the resource\n";
sem.release(); // 释放资源权限
}

int main() {
std::vector<std::thread> threads(5);
for(int i = 0; i < 5; i ++ ) threads[i] = std::thread(foo, i);
for(auto& t : threads) t.join();

return 0;
}

可能的运行结果如下:

1
2
3
4
5
6
7
8
9
10
Thread Thread 3 is accessing the resource
0 is accessing the resource
Thread 0 is releasing the resource
Thread 3 is releasing the resource
Thread 4 is accessing the resource
Thread 1 is accessing the resource
Thread 1 is releasing the resource
Thread 4 is releasing the resource
Thread 2 is accessing the resource
Thread 2 is releasing the resource

从上面的结果可以看出,一开始线程 0 和线程 3 同时访问资源,其他线程处于阻塞状态;然后线程 4 和线程 1 同时访问资源,线程 2 被阻塞;最后线程 2 访问资源。

2.4 应用场景

  • 管理多个线程访问少量的共享资源。
  • 控制线程池中活跃线程的数量。
  • 防止过多的线程同时访问导致资源枯竭或系统不稳定。

3 std::binary_semaphore

3.1 基本概念

std::binary_semaphore 是一种同步原语,在 C++20 中引入,位于 <semaphore> 头文件中。

std::binary_semaphore 实际上是 std::counting_semaphore 的一种特化,最大计数为 1,其只有两个状态,0(未锁定)和1(锁定)功能,类似于一个简易的互斥锁(std::mutex)。

主要用途是在线程之间协调共享资源的访问,确保只有一个线程在某一时刻访问资源,类似于互斥锁(mutex),但语义稍有不同。

3.2 常规操作

只能用初始计数为 0 或 1 来初始化。

  • 0:信号量初始时是锁定的,所以第一次 acquire() 调用将被阻塞。
  • 1:信号量初始时是未锁定的,所以第一次 acquire() 调用将不会被阻塞。

3.3 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <semaphore>
#include <thread>
#include <chrono>

std::binary_semaphore sm1(0), sm2(0);

void foo(){
sm1.acquire(); // 等待主进程的信号

std::cout << "foo(): Got the signal\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "foo(): Send the signal\n";

sm2.release(); // 向主进程发出返回信号
}

int main(){
std::thread t(foo);

std::cout << "main(): Send the signal\n";
sm1.release(); // 向子线程发出信号可以开始工作
sm2.acquire(); // 等待子线程完成工作
std::cout << "main(): Got the signal\n";

t.join();

return 0;
}

在上面的代码中,这里定义了两个二进制信号量 sm1sm2,初始计数都为0,这意味着它们开始都是在“锁定”状态。

执行顺序如下:

  1. 主线程启动子线程。
  2. 主线程通过 sm1.release() 向子线程发出信号。
  3. 子线程从 sm1.acquire() 中解除阻塞,开始其任务。
  4. 子线程完成任务后,通过 sm2.release() 向主线程发送完成信号。
  5. 主线程从 sm2.acquire() 中解除阻塞,继续执行。

可能的运行结果如下:

1
2
3
4
main(): Send the signal
foo(): Got the signal
foo(): Send the signal
main(): Got the signal

可以看出通过信号量机制,子线程确保在主线程发出信号执行,并在主线程尝试获取信号时执行完毕。

3.4 优势

  • 效率:由于其轻量级的特性,std::binary_semaphore 比互斥锁在某些情况下 更高效

  • 适用性:非常适合控制单次访问的资源,尤其是在线程之间进行信号传递时,与条件变量类似,但通常更易于使用。

参考

  1. std::counting_semaphore, std::binary_semaphore

  2. Runebook.dev std::counting_semaphore, std::binary_semaphore


「C++ 多线程」信号量 counting_semaphore 与 binary_semaphore
https://marisamagic.github.io/2025/01/13/20250113/
作者
MarisaMagic
发布于
2025年1月13日
许可协议