C++信号量semaphore实现同步:原理、应用与代码示例
在多线程编程中,同步是一个至关重要的问题。多个线程同时访问共享资源时,如果没有适当的同步机制,就可能会导致数据不一致、竞态条件等问题。C++ 提供了多种同步工具,其中信号量(semaphore)是一种强大且灵活的同步原语。本文将深入探讨 C++ 中信号量的原理、如何使用信号量实现线程同步,以及相关的代码示例。
信号量的基本概念
信号量是由荷兰计算机科学家 Edsger W. Dijkstra 在 1965 年提出的一种同步机制。它本质上是一个计数器,用于控制对共享资源的访问。信号量有两种基本操作:P 操作(也称为 wait 操作)和 V 操作(也称为 signal 操作)。
-
P 操作:当一个线程执行 P 操作时,它会检查信号量的值。如果信号量的值大于 0,则将信号量的值减 1,并继续执行;如果信号量的值为 0,则线程会被阻塞,直到信号量的值大于 0。
-
V 操作:当一个线程执行 V 操作时,它会将信号量的值加 1。如果有其他线程因为信号量值为 0 而被阻塞,那么其中一个线程会被唤醒。
在 C++ 20 之前,标准库没有提供信号量的实现,开发者通常使用操作系统提供的 API 或第三方库来实现信号量。而从 C++ 20 开始,标准库正式引入了信号量的实现,包括计数信号量(counting semaphore)和二进制信号量(binary semaphore)。
C++ 20 中信号量的实现
计数信号量
计数信号量可以有任意非负整数值,表示可以同时访问共享资源的线程数量。在 C++ 中,可以使用 std::counting_semaphore 来创建计数信号量。
#include <iostream>
#include <thread>
#include <semaphore>
// 定义一个计数信号量,初始值为 2
std::counting_semaphore<2> sem(2);
void worker(int id) {
// P 操作,获取信号量
sem.acquire();
std::cout << "Thread " << id << " is working." << std::endl;
// 模拟工作
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Thread " << id << " has finished." << std::endl;
// V 操作,释放信号量
sem.release();
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
std::thread t3(worker, 3);
t1.join();
t2.join();
t3.join();
return 0;
}
在上述代码中,我们定义了一个初始值为 2 的计数信号量 sem,这意味着最多有 2 个线程可以同时访问共享资源。每个线程在开始工作前会调用 sem.acquire() 进行 P 操作,工作完成后会调用 sem.release() 进行 V 操作。
二进制信号量
二进制信号量是计数信号量的一种特殊情况,它的值只能是 0 或 1,通常用于实现互斥锁。在 C++ 中,可以使用 std::binary_semaphore 来创建二进制信号量。
#include <iostream>
#include <thread>
#include <semaphore>
// 定义一个二进制信号量,初始值为 1
std::binary_semaphore sem(1);
void critical_section(int id) {
// P 操作,获取信号量
sem.acquire();
std::cout << "Thread " << id << " is in the critical section." << std::endl;
// 模拟临界区操作
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Thread " << id << " is leaving the critical section." << std::endl;
// V 操作,释放信号量
sem.release();
}
int main() {
std::thread t1(critical_section, 1);
std::thread t2(critical_section, 2);
t1.join();
t2.join();
return 0;
}
在这个例子中,我们使用二进制信号量来保护临界区,确保同一时间只有一个线程可以进入临界区。
信号量的应用场景
生产者 - 消费者模型
生产者 - 消费者模型是多线程编程中一个经典的同步问题。在这个模型中,生产者线程负责生产数据,消费者线程负责消费数据。信号量可以用来实现生产者和消费者之间的同步。
#include <iostream>
#include <thread>
#include <semaphore>
#include <queue>
std::queue<int> buffer;
std::counting_semaphore<5> empty(5);
std::counting_semaphore<0> full(0);
void producer() {
for (int i = 0; i < 10; ++i) {
// 等待缓冲区有空闲位置
empty.acquire();
{
buffer.push(i);
std::cout << "Produced: " << i << std::endl;
}
// 通知消费者有新数据
full.release();
}
}
void consumer() {
for (int i = 0; i < 10; ++i) {
// 等待缓冲区有数据
full.acquire();
{
int item = buffer.front();
buffer.pop();
std::cout << "Consumed: " << item << std::endl;
}
// 通知生产者有空闲位置
empty.release();
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
在这个代码中,empty 信号量表示缓冲区中的空闲位置数量,full 信号量表示缓冲区中的数据数量。生产者线程在生产数据前会先获取 empty 信号量,生产完成后会释放 full 信号量;消费者线程在消费数据前会先获取 full 信号量,消费完成后会释放 empty 信号量。
总结与建议
信号量是一种强大的同步原语,在多线程编程中有着广泛的应用。C++ 20 引入的信号量标准库为开发者提供了方便的工具,使得信号量的使用更加简单和安全。
在使用信号量时,需要注意以下几点:
- 合理设置信号量的初始值:信号量的初始值决定了可以同时访问共享资源的线程数量,需要根据具体的应用场景进行合理设置。
- 避免死锁:在使用多个信号量时,需要注意避免死锁的发生。死锁通常是由于线程之间互相等待对方释放信号量而导致的。
- 注意信号量操作的顺序:P 操作和 V 操作的顺序非常重要,错误的顺序可能会导致程序出现逻辑错误。
总之,掌握信号量的原理和使用方法,可以帮助开发者更好地解决多线程编程中的同步问题,提高程序的性能和稳定性。

