一、基本概念
1.1 概念
信号量(Semaphore)是一种用于控制对共享资源访问的同步机制。它通常用于多线程或多进程编程中,以避免竞态条件和数据不一致的问题。信号量可以看作是一个计数器,用于跟踪可以访问某个特定资源的线程或进程的数量。
注:【当创建的信号量的个数为1时,则和互斥锁功能类似,下文将详细介绍】
1.2 基本操作
信号量主要有两种操作:
-
P(等待)操作: 也称为down或acquire操作。当线程或进程需要访问某个资源时,它会执行P操作。如果信号量的值大于0,则将其减1,表示有一个资源被占用,线程或进程可以继续执行。如果信号量的值为0,则线程或进程将被阻塞,直到信号量的值大于0。
-
V(信号)操作: 也称为up或release操作。当线程或进程完成对资源的访问后,它会执行V操作,将信号量的值加1,表示释放了一个资源。如果有其他线程或进程因为信号量的值为0而被阻塞,那么其中一个(或多个,取决于具体的实现)将被唤醒以继续执行。
1.3 使用场景
-
进程同步
-
临界资源的互斥访问
-
生产-消费者问题
-
线程池管理
-
顺序控制
-
中断与线程同步
-
优先级反转问题
本文暂时介绍常用的POSIX兼容系统中信号量的基本函数,针对不同的使用场景,后续持续更新说明和示例代码。
二、基本函数【POSIX C语言】
信号量的基本函数定义在<semaphore.h>
头文件中。
2.1 初始化一个未命名的信号量【内存】
sem_init
函数用于初始化一个未命名的信号量(unnamed semaphore),该信号量只能在初始化它的进程内部被访问和共享。
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数说明:
-
sem:指向要初始化的信号量对象的指针
-
pshared:一个整数,用于指定信号量是否应该在进程间共享。如果其值为0,则信号量仅在当前进程中可用;如果其值非0,则信号量在进程间共享(但请注意,由于
sem_init
创建的信号量是未命名的,实际上在多个进程间共享这样的信号量需要其他机制,如通过共享内存映射)。 -
value:信号量的初始值
-
返回值:0初始化成功,其他为失败
特点:
- 适用于线程间同步或单个进程内的同步。
- 不通过名称共享,因此不能直接在多个进程间共享(除非通过其他机制如共享内存)。
- 可以通过
sem_destroy
函数销毁信号量。
2.2 创建初始化一个新的有名信号量
sem_open
函数用于创建并初始化一个新的有名信号量(named semaphore),或者打开一个已存在的有名信号量。有名信号量可以在进程间共享,通过其名称来标识。
#include <semaphore.h>
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
sem_t *sem_open(const char *name, int oflag, ...);
// 第三个参数是可选的,通常用于指定权限(mode)和初始值(value),但具体取决于oflag的值
参数说明:
-
name:指定信号量的名称,通常是一个以
/
开头的文件系统中的路径名。 -
oflag:操作标志,包括
O_CREAT
(如果信号量不存在则创建它)、O_EXCL
(与O_CREAT
一起使用,确保信号量是新创建的)等。 -
…:可变参数列表,通常包括
mode_t mode
(文件权限,当O_CREAT
被设置时)和unsigned int value
(信号量的初始值)
特点:
- 适用于进程间同步。
- 通过名称在进程间共享。
- 可以通过
sem_unlink
函数删除有名信号量。
2.3 sem_init和sem_open的区别
可根据使用场景选择。
sem_open | sem_init | |
---|---|---|
用途 | 创建并初始化有名信号量,或打开已存在的有名信号量 | 初始化未命名的信号量 |
进程间共享 | 是(通过名称) | 否(除非通过其他机制如共享内存) |
创建与打开 | 创建新信号量或打开已存在的信号量 | 仅初始化信号量 |
删除 | 使用sem_unlink | 使用sem_destroy |
适用场景 | 进程间同步 | 线程间同步或单个进程内的同步 |
2.4 P操作【等待操作】
等待(阻塞)信号量变为非零,然后将它的值减1。
int sem_wait(sem_t *sem);
参数说明同上。
2.5 V操作【信号操作】
将信号量的值加1,并唤醒一个等待该信号量的线程(如果有的话)
int sem_post(sem_t *sem);
参数说明同上。
2.6 获取信号量当前值
用于获取信号量(semaphore)的当前值。
注意:当调用 sem_getvalue()
时,它会将 sem
指向的信号量的当前值写入 sval
指向的整数中。这个函数通常用于调试或系统状态的检查,但不建议用来控制程序的逻辑,因为在多线程或多进程环境中,信号量的值可能在检查后立即改变。
int sem_getvalue(sem_t *sem, int *sval);
参数说明:
-
sem:指向你想要获取其值的信号量对象的指针
-
sval:指向整数的指针,函数执行后,该整数将包含信号量的当前值
-
返回值:成功时,函数返回 0,失败则会设置errno表示错误原因
注意事项:
- 多线程/多进程环境:在多线程或多进程环境中,信号量的值可能在
sem_getvalue()
调用后立即改变,因此该函数的结果可能并不总是可靠的,特别是在需要精确控制同步逻辑的场景中。 - 可移植性问题:尽管
sem_getvalue()
在许多遵循 POSIX 标准的系统上可用,但并非所有系统都保证会返回信号量的准确值。某些系统可能会因为性能考虑而返回一个估计值或特定值(如 0)。 - 用途限制:
sem_getvalue()
主要用于调试和监视目的,而不应作为程序逻辑的关键部分。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
int main() {
sem_t sem;
int value;
// 初始化信号量
if (sem_init(&sem, 0, 5) == -1) {
perror("sem_init");
exit(EXIT_FAILURE);
}
// 获取信号量的当前值
if (sem_getvalue(&sem, &value) == -1) {
perror("sem_getvalue");
sem_destroy(&sem);
exit(EXIT_FAILURE);
}
printf("Current value of semaphore: %d\n", value);
// 销毁信号量
sem_destroy(&sem);
return 0;
}
结果为:
代码说明:
在这个示例中,我们首先初始化了一个信号量,并将其初始值设置为 5。然后,我们使用 sem_getvalue()
获取信号量的当前值,并将其打印出来。最后,我们销毁信号量并退出程序。需要注意的是,由于信号量的初始值就是 5,因此在这个特定的示例中,sem_getvalue()
将返回 5。然而,在更复杂的场景中,信号量的值可能会因为其他线程或进程的操作而改变。
2.7 销毁信号量
int sem_destroy(sem_t *sem);
2.8 其他不常用函数
//尝试对信号量执行wait操作,但如果信号量的值为0,则不会阻塞调用线程,而是立即返回。
sem_trywait(sem_t *sem);
#include <semaphore.h>
#include <time.h>
/*
* 函数说明:该函数对信号量执行减操作(通常称为wait或P操作)。如果信号量的值大于0,则函数将信号量的值减1并立即返回;如果信号量的值为0,则调用线程将被阻塞,直到信号量的值变为非零或达到指定的超时时间。如果达到超时时间而信号量仍未被释放(即其值仍为0),则函数返回-1,并设置errno为ETIMEDOUT,表示已经超时。
* 参数说明:
* abs_timeout:指向timespec结构的指针,
* 它定义了一个绝对超时时间。
* 这个时间是自UTC 1970-01-01 00:00:00 +0000(Epoch)开始计算的秒数和纳秒数。
*/
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
/*
* 函数说明:
* 关闭一个通过sem_open()打开的POSIX有名信号量。
* 关闭信号量并不会删除它,也不会释放其占用的资源,只是关闭了与信号量相关联的文件描述符。
* 在进程结束时,所有打开的文件描述符都会被自动关闭,包括通过sem_open()打开的信号量。
* 但是,显式调用sem_close()可以更早地释放资源,并允许系统重用这些资源。
*/
int sem_close(sem_t *sem);
/*
* 函数说明:用于从系统中删除一个POSIX有名信号量。
* 调用此函数后,信号量的名称将不再与任何信号量对象相关联,且该信号量对象将被销毁(如果它已没有打开的文件描述符)。
* 一旦信号量被sem_unlink()删除,任何尝试通过其名称来打开或访问它的操作都将失败。
* 参数说明:name:指定要删除的信号量的名称
*/
int sem_unlink(const char *name);
三、代码示例
下文是一个使用POSIX信号量的C语言示例,该示例演示了如何使用信号量来控制对共享资源的访问:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t sem;
void* thread_func(void* arg) {
// 等待信号量
sem_wait(&sem);
// 访问共享资源(这里只是简单地打印一条消息)
printf("Thread %ld is accessing the resource\n", (long)arg);
sleep(1); // 模拟资源访问时间
// 释放信号量
sem_post(&sem);
return NULL;
}
int main() {
pthread_t t1, t2;
// 初始化信号量,初始值为1,表示一次只允许一个线程访问共享资源
if (sem_init(&sem, 0, 1) != 0) {
perror("Semaphore initialization failed");
exit(EXIT_FAILURE);
}
// 创建两个线程
if (pthread_create(&t1, NULL, thread_func, (void*)1) != 0) {
perror("Thread 1 creation failed");
exit(EXIT_FAILURE);
}
if (pthread_create(&t2, NULL, thread_func, (void*)2) != 0) {
perror("Thread 2 creation failed");
exit(EXIT_FAILURE);
}
// 等待两个线程完成
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 销毁信号量
sem_destroy(&sem);
return 0;
}
在这个示例中,我们创建了一个信号量sem
,其初始值为1,表示一次只允许一个线程访问共享资源。我们创建了两个线程t1
和t2
,它们都会尝试访问共享资源(在这个示例中,只是简单地打印一条消息)。每个线程在访问共享资源之前都会调用sem_wait()
来等待信号量,访问完成后会调用sem_post()
来释放信号量。这样,即使两个线程几乎同时运行,它们也不会同时访问共享资源,从而避免了数据竞争的问题。
运行结果为:
后续将针对不同的应用场景介绍信号量的使用。