C++信号量semaphore实现同步:原理、应用与代码示例

2026-03-18 02:30:02 8385阅读

在多线程编程中,同步是一个至关重要的问题。多个线程同时访问共享资源时,如果没有适当的同步机制,就可能会导致数据不一致、竞态条件等问题。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 引入的信号量标准库为开发者提供了方便的工具,使得信号量的使用更加简单和安全。

在使用信号量时,需要注意以下几点:

  1. 合理设置信号量的初始值:信号量的初始值决定了可以同时访问共享资源的线程数量,需要根据具体的应用场景进行合理设置。
  2. 避免死锁:在使用多个信号量时,需要注意避免死锁的发生。死锁通常是由于线程之间互相等待对方释放信号量而导致的。
  3. 注意信号量操作的顺序:P 操作和 V 操作的顺序非常重要,错误的顺序可能会导致程序出现逻辑错误。

总之,掌握信号量的原理和使用方法,可以帮助开发者更好地解决多线程编程中的同步问题,提高程序的性能和稳定性。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

目录[+]