目录
一、信号量(Semaphores)
信号量概念
定义: 信号量是一种经典的同步原语,用于控制多个线程或进程对共享资源的访问。它是一个带有整数值的变量,通过特定的原子操作进行增加(称为“信号”或“发布”,如sem_post()
)和减少(称为“等待”或“获取”,如sem_wait()
)。信号量的值代表了可用资源的数量或访问权限的许可。
分类:
-
计数型信号量:也称作通用信号量或计数信号量,其值可以取任意非负整数。它适用于管理一组可重入的资源,如数据库连接池或文件句柄池。计数型信号量的值表示资源池中当前可用资源的数量。
-
二值型信号量:又称为二进制信号量或互斥锁,其值只能为0或1。当值为1时,表示资源可用;值为0时,表示资源已被占用。二值型信号量常用于实现对单一共享资源的互斥访问,类似于传统的互斥锁(Mutex)。
在并发控制中的应用场景:
-
资源池管理:如控制数据库连接、文件句柄、网络套接字等有限资源的分配和回收,防止过多的并发请求耗尽资源。
-
互斥访问:确保同一时刻只有一个线程能够访问临界区(如全局数据结构),防止数据竞争和一致性破坏。
-
同步事件:协调多个线程之间的执行顺序,例如在一个线程完成特定任务后,通过信号量通知其他线程继续执行。
-
生产者-消费者问题:在多生产者多消费者场景中,信号量可以用于控制队列或缓冲区的填充和消耗,确保生产者不会过度填充,消费者也不会尝试访问空队列。
C语言中信号量的使用
a. 信号量数据类型与API介绍
在POSIX兼容的C语言环境中,信号量通常由以下数据类型和API函数组成:
-
信号量数据类型:
sem_t
,表示一个信号量对象。在使用前需要通过sem_init()
进行初始化。 -
信号量API:
-
初始化:
sem_init(sem_t *sem, int pshared, unsigned int value)
,创建并初始化一个信号量。pshared
参数决定信号量是否能在进程间共享(非零值表示可共享),value
为初始计数值。 -
发布(增加):
sem_post(sem_t *sem)
,将信号量值递增1,表示资源可用性增加或访问许可增多。如果其他线程正在等待此信号量,那么至少会唤醒一个等待线程。 -
等待(减少):
sem_wait(sem_t *sem)
,将信号量值递减1。如果当前值大于0,则直接递减并返回;若当前值为0,则阻塞当前线程,直到其他线程通过sem_post()
增加信号量值。 -
尝试等待:
sem_trywait(sem_t *sem)
,尝试减少信号量而不阻塞。如果信号量值为0,函数立即返回错误(通常为EAGAIN
或EBUSY
)。 -
获取当前值(可选):某些实现可能提供如
sem_getvalue(sem_t *sem, int *sval)
的函数,用于获取信号量的当前值,但并非所有系统都支持此功能。 -
销毁:
sem_destroy(sem_t *sem)
,在信号量不再使用时,释放其占用的系统资源。必须确保没有线程正在该信号量上等待。
-
b. 信号量操作示例
以下是一个使用信号量控制并发打印任务的简单示例:
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <stdlib.h>
sem_t print_semaphore; // 定义一个信号量
void* printing_thread(void* arg) {
int id = *((int*)arg);
for (int i = 0; i < 5; ++i) {
sem_wait(&print_semaphore); // 获取打印许可
printf("Thread %d: Printing task %d\n", id, i);
fflush(stdout);
sleep(1); // 模拟打印任务耗时
sem_post(&print_semaphore); // 释放打印许可
}
return NULL;
}
int main() {
sem_init(&print_semaphore, 0, 1); // 初始化信号量,限制同时打印的任务数为1
pthread_t threads[3];
int ids[] = {1, 2, 3};
for (int i = 0; i < 3; ++i) {
pthread_create(&threads[i], NULL, printing_thread, &ids[i]);
}
for (int i = 0; i < 3; ++i) {
pthread_join(threads[i], NULL);
}
sem_destroy(&print_semaphore); // 销毁信号量
return 0;
}
在这个示例中,一个全局的二值型信号量用于限制同时打印的任务数为1,确保任何时候只有一个线程在执行打印任务,实现了打印任务的互斥访问。
c. 信号量与互斥锁、条件变量的对比
-
互斥锁(Mutex):
- 优点:简单易用,适用于实现对单个资源的互斥访问。
- 缺点:功能相对单一,不能直接用于控制多个资源的并发访问或实现复杂的同步逻辑。
- 适用场景:基本的临界区保护,对单一共享资源的访问控制。
-
条件变量(Condition Variables):
- 优点:能够实现更为精细的线程同步,允许线程在特定条件不满足时阻塞等待,并在条件变化时精确唤醒相关线程。
- 缺点:使用复杂度较高,需要配合互斥锁,且容易出现假唤醒和循环等待问题。
- 适用场景:复杂的线程间同步,如生产者-消费者模式、读者-写者问题等,需要等待特定条件满足后再继续执行的场景。
-
信号量(Semaphores):
- 优点:既可以实现互斥访问(二值型),也能控制对多个资源的并发访问(计数型)。适用于资源池管理、多线程同步等多种场景。
- 缺点:相比互斥锁,使用信号量可能导致更多的上下文切换,且如果使用不当,可能会导致线程饥饿或优先级反转等问题。
- 适用场景:资源池管理、多线程同步、限制并发任务数、解决生产者-消费者问题等。
总结来说,互斥锁、条件变量和信号量各有优缺点,选择使用哪一种取决于具体的并发控制需求。互斥锁适用于简单的互斥访问,条件变量适用于复杂的线程间同步,而信号量则更通用,既可用于互斥,也可用于控制并发访问多个资源。在实际编程中,往往需要结合使用这些同步机制来构建高效且正确的并发程序。