信号,信号量,条件变量三者辨析

1.信号(Signal)

  • 概念
    • 信号是一种软中断机制,用于通知进程发生了某个特定的事件。它是一种异步事件通知方式,进程在收到信号时可以采取相应的动作,如终止进程、暂停进程或者忽略信号等。信号是由操作系统内核发送给进程的。
  • 举例
    • 当用户在终端中按下Ctrl + C组合键时,内核会向当前正在运行的前台进程发送一个SIGINT(中断信号)。如果进程没有对这个信号进行特殊处理,默认情况下会终止运行。例如,下面是一个简单的 C 程序来处理SIGINT信号:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void signal_handler(int signum) {
printf("Received signal %d\n", signum);
// 在这里可以进行更复杂的处理,比如清理资源等
}

int main() {
// 注册信号处理函数
if (signal(SIGINT, signal_handler) == SIG_ERR) {
perror("signal");
return 1;
}
// 让程序进入一个循环,等待信号
while (1) {
// 可以在这里进行其他操作
}
return 0;
}

在这个例子中,通过signal函数注册了SIGINT信号的处理函数signal_handler。当接收到SIGINT信号时,就会执行signal_handler函数,而不是默认的终止进程。

2.信号量(Semaphore)

  • 概念
    • 信号量是一种用于进程同步和互斥的机制。它本质上是一个非负整数计数器,用于控制对共享资源的访问。信号量有两个基本操作:P操作(也称为wait操作)和V操作(也称为signal操作)。P操作会将信号量的值减 1,如果信号量的值小于 0,则进程会被阻塞;V操作会将信号量的值加 1,如果信号量的值小于等于 0,则会唤醒一个被阻塞的进程。
  • 举例
    • 假设有一个共享缓冲区,多个生产者进程向缓冲区中写入数据,多个消费者进程从缓冲区中读取数据。为了避免同时访问缓冲区导致数据不一致,我们可以使用信号量来进行同步。
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

// 定义信号量操作结构体
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};

// P操作函数
void P(int sem_id) {
struct sembuf sem_op;
sem_op.sem_num = 0;
sem_op.sem_op = -1;
sem_op.sem_flg = 0;
if (semop(sem_id, &sem_op, 1) == -1) {
perror("P operation");
exit(1);
}
}

// V操作函数
void V(int sem_id) {
struct sembuf sem_op;
sem_op.sem_num = 0;
sem_op.sem_op = 1;
sem_op.sem_flg = 0;
if (semop(sem_id, &sem_op, 1) == -1) {
perror("V operation");
exit(1);
}
}

int main() {
// 创建信号量集,只有一个信号量
int sem_id = semget(IPC_PRIVATE, 1, 0666);
if (sem_id == -1) {
perror("semget");
return 1;
}
// 初始化信号量的值为1,表示资源可用
union semun arg;
arg.val = 1;
if (semctl(sem_id, 0, SETVAL, arg) == -1) {
perror("semctl");
return 1;
}
// 创建子进程(生产者或消费者),这里简单示意,实际可能有多个
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程作为生产者
P(sem_id);
// 访问共享资源(这里简单打印表示写入缓冲区)
printf("Producer: Writing to buffer\n");
V(sem_id);
} else {
// 父进程作为消费者
P(sem_id);
// 访问共享资源(这里简单打印表示读取缓冲区)
printf("Consumer: Reading from buffer\n");
V(sem_id);
}
// 标记信号量以便删除
if (semctl(sem_id, 0, IPC_RMID) == -1) {
perror("semctl");
return 1;
}
return 0;
}

在这个例子中,通过semget函数创建了一个信号量,通过semctl函数初始化信号量的值为 1。生产者进程和消费者进程在访问共享资源(模拟的缓冲区)之前,都需要执行P操作获取信号量,如果信号量的值为 0(表示资源被占用),则会被阻塞。访问完资源后,执行V操作释放信号量,使得其他进程可以获取信号量来访问资源。

3.条件变量(Condition Variable)

  • 概念
    • 条件变量是用于线程同步的一种机制,它允许线程等待某个条件成立。条件变量通常和互斥锁一起使用。线程在等待条件变量时会被阻塞,当其他线程改变了条件并发出信号(signalbroadcast操作)时,被阻塞的线程会被唤醒,重新检查条件是否满足。
  • 举例
    • 假设有一个线程池,工作线程等待任务队列中有任务时才开始工作。
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 <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mutex_queue;
std::condition_variable cond_var;
std::queue<int> task_queue;

// 工作线程函数
void worker_thread() {
std::unique_lock<std::mutex> lock(mutex_queue);
while (true) {
// 等待任务队列中有任务
cond_var.wait(lock, [] { return!task_queue.empty(); });
int task = task_queue.front();
task_queue.pop();
lock.unlock();
// 执行任务
std::cout << "Thread is working on task: " << task << std::endl;
// 可以在这里进行实际的任务处理,比如复杂的计算等
lock.lock();
}
}

int main() {
// 创建多个工作线程
const int num_threads = 3;
std::thread threads[num_threads];
for (int i = 0; i < num_threads; ++i) {
threads[i] = std::thread(worker_thread);
}
// 主线程向任务队列中添加任务
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mutex_queue);
task_queue.push(i);
lock.unlock();
cond_var.notify_one();
}
// 等待所有线程完成任务(这里简单示意,实际可能需要更好的退出机制)
for (int i = 0; i < num_threads; ++i) {
threads[i].join();
}
return 0;
}

在这个例子中,工作线程通过cond_var.wait函数等待任务队列中有任务。主线程在向任务队列添加任务后,通过cond_var.notify_one函数唤醒一个等待的工作线程来执行任务。互斥锁mutex_queue用于保护任务队列的并发访问,保证在检查任务队列是否为空和添加 / 取出任务时的原子性。

4.相同点

  • 目的相似:信号、信号量和条件变量都是用于在多进程或多线程环境下进行协调和同步的机制,以避免竞争条件和确保程序的正确执行。
  • 基于操作系统支持:它们都依赖于操作系统提供的底层机制来实现功能,例如信号是由内核发送和处理,信号量和条件变量的操作也是通过系统调用或操作系统提供的库函数来实现。
  • 与并发编程相关:在并发编程场景中,无论是进程还是线程之间的交互,都可能会用到这些机制来处理共享资源的访问、事件通知等问题。

5.不同点

  • 功能重点
    • 信号主要用于异步事件通知,进程接收到信号后可以执行预定义的动作来响应事件。信号量重点在于对共享资源的访问控制,通过计数来限制同时访问资源的进程或线程数量。条件变量主要用于线程等待某个特定条件成立,它和互斥锁配合使用,更关注于条件的等待和唤醒机制。
  • 应用场景
    • 信号适用于处理外部事件(如用户输入、硬件中断等)对进程的影响。信号量常用于多个进程或线程对有限的共享资源(如缓冲区、设备等)的访问控制。条件变量主要用于线程之间基于条件的同步,比如等待某个数据结构满足一定条件(如队列非空等)。
  • 操作方式
    • 信号是由内核发送给进程,进程可以选择忽略、默认处理或自定义处理信号。信号量有PV操作来控制计数器的值,从而控制进程或线程的阻塞和唤醒。条件变量通过waitsignalbroadcast操作来让线程等待条件和唤醒等待条件的线程。