并发编程中,信号量(Semaphore)作为一种强大的同步工具,可以有效地控制多个线程或进程对共享资源的访问。C++ 提供了信号量的基本操作接口,使开发者能够方便地在多线程或多进程环境中实现安全的数据共享。在不同的场景下,信号量的配置,如初始值(value)和操作标志(sem_flg),需要根据具体需求进行合理设置。


一、信号量的基本概念

信号量可以看作一个受保护的计数器,它能够控制对资源的并发访问。信号量的值通常表示当前可用资源的数量,并通过两种基本操作来管理资源的访问:

  • P 操作(等待操作,Wait/Decrement):将信号量的值减 1。如果信号量的值已经为 0,操作将阻塞,等待信号量的值增加。
  • V 操作(信号操作,Signal/Increment):将信号量的值加 1,解除阻塞等待该资源的进程或线程。

二、互斥锁中的信号量配置

2.1 互斥锁的作用

互斥锁(Mutex)用于确保在同一时刻,只有一个线程或进程能够访问共享资源,从而防止数据竞争。互斥锁的典型应用场景包括对临界区的保护,避免多线程同时访问并修改共享数据,从而造成数据不一致或未定义行为。

2.2 信号量在互斥锁中的配置

在实现互斥锁时,信号量的初始值和操作标志 sem_flg 的配置非常关键:

  • value 初始值为 1:在互斥锁的场景中,信号量的初始值通常设为 1,表示共享资源是可用的。线程或进程进入临界区时执行 P 操作,信号量的值减 1。当信号量的值为 0 时,意味着资源被占用,其他线程将阻塞,等待资源释放。退出临界区后,执行 V 操作释放资源。

  • sem_flg 设置为 SEM_UNDO:该标志确保在持有锁的进程意外崩溃或退出时,操作系统能够自动撤销该进程对信号量的修改,释放锁资源。这样可以避免死锁现象,确保系统能够继续正常运行。

2.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
30
31
32
33
34
35
36
37
#include <iostream>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>

class Semaphore {
public:
    Semaphore(key_t key, int initial_value) {
        semid = semget(key, 1, 0666 | IPC_CREAT);
        semctl(semid, 0, SETVAL, initial_value);
    }

    void wait() {
        struct sembuf sb = {0, -1, SEM_UNDO};  // P 操作
        semop(semid, &sb, 1);
    }

    void post() {
        struct sembuf sb = {0, 1, SEM_UNDO};   // V 操作
        semop(semid, &sb, 1);
    }

private:
    int semid;
};

int main() {
    key_t key = ftok("semfile", 'a');
    Semaphore mutex(key, 1);  // 互斥锁的信号量,初始值为 1

    mutex.wait();  // 进入临界区
    std::cout << "进入临界区,进行独占操作" << std::endl;
    sleep(2);      // 模拟处理
    mutex.post();  // 退出临界区

    return 0;
}

三、生产者-消费者模型中的信号量配置

3.1 生产者-消费者模型的原理

生产者-消费者模型是一种经典的并发控制模式,其中生产者生成资源并放入缓冲区,消费者从缓冲区获取资源进行处理。为了避免生产者过度填充缓冲区或消费者在没有可用资源时阻塞,通常需要使用信号量来管理缓冲区的状态:

  • empty_slots 信号量:用于表示缓冲区中的空闲位置。
  • full_slots 信号量:用于表示缓冲区中的已占用位置。

3.2 信号量在生产者-消费者模型中的配置

在生产者-消费者模型中,信号量的初始值和操作标志 sem_flg 需要根据模型的具体需求进行合理配置:

  • value 的配置

    • empty_slots 初始值为缓冲区大小:该信号量表示缓冲区中的空闲位置数,因此初始值为缓冲区的总大小。当生产者生成资源并放入缓冲区时,empty_slots 的值减 1;当消费者取走资源时,empty_slots 的值加 1,表示腾出一个空闲位置。
    • full_slots 初始值为 0:该信号量表示缓冲区中已占用位置的数量,初始值为 0,表示开始时缓冲区为空。每当生产者放入一个资源,full_slots 的值加 1;消费者取走资源时,该值减 1。
  • sem_flg 的配置:在生产者-消费者模型中,sem_flg 通常设置为 0。因为信号量的状态完全由程序控制,系统不会自动撤销进程的信号量修改,确保信号量的值与缓冲区状态保持一致。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>

class Semaphore {
public:
    Semaphore(key_t key, int initial_value) {
        semid = semget(key, 1, 0666 | IPC_CREAT);
        semctl(semid, 0, SETVAL, initial_value);
    }

    void wait() {
        struct sembuf sb = {0, -1, 0};  // P 操作
        semop(semid, &sb, 1);
    }

    void post() {
        struct sembuf sb = {0, 1, 0};   // V 操作
        semop(semid, &sb, 1);
    }

private:
    int semid;
};

int main() {
    key_t key_empty = ftok("emptyfile", 'a');
    key_t key_full = ftok("fullfile", 'b');
    int buffer_size = 5;

    Semaphore empty_slots(key_empty, buffer_size);  // 空闲槽位,初始值为缓冲区大小
    Semaphore full_slots(key_full, 0);              // 已占用槽位,初始值为 0

    // 生产者逻辑
    empty_slots.wait();  // 等待空闲位置
    std::cout << "生产者生成资源" << std::endl;
    full_slots.post();   // 通知消费者资源可用

    // 消费者逻辑
    full_slots.wait();   // 等待可消费资源
    std::cout << "消费者消费资源" << std::endl;
    empty_slots.post();  // 通知生产者有空闲位置

    return 0;
}

四、不同场景中信号量配置的比较

4.1 value 的配置差异

  • 互斥锁中的 value 为 1:表示资源是可用的,只有一个线程可以进入临界区。此时信号量的值为 0 表示资源被占用。
  • 生产者-消费者模型中的 valueempty_slots 表示缓冲区的可用空间,因此初始值为缓冲区的大小;full_slots 初始值为 0,表示开始时没有可供消费的资源。

4.2 sem_flg 的配置差异

  • 互斥锁中的 sem_flgSEM_UNDO:确保在进程异常退出时,操作系统能够撤销对信号量的修改,避免资源死锁。
  • 生产者-消费者模型中的 sem_flg 为 0:信号量完全由程序控制,避免操作系统干预信号量的值,以确保信号量与缓冲区状态一致。