Linux匿名信号量详细介绍
回顾POSIX命名信号,互斥锁,读写锁
在这篇文章之前我们已经学习过了POSIX命名信号,互斥锁,读写锁等多种同步互斥的机制。其中POSIX命名信号是用在进程间的,互斥锁和读写锁是用在线程间的。
对于posix命名信号量来说:
- 命名信号量不需要手动定义变量,它由系统在文件系统中(通常是
/dev/shm
目录)创建一个特殊的文件。 - 通过
sem_open()
函数指定一个字符串名称来创建或打开一个命名信号量。 - 用于进程之间的同步。
sem_open()
函数在创建时会进行初始化。- 使用
sem_wait()
和sem_post()
函数进行等待和释放操作。 - 值得注意的是,命名信号量的生命周期和进程不同步,POSIX命名信号量的生命由操作系统管控,所以退出进程命名信号量不会自动销毁,需要我们编写代码手动销毁,不然占用空间且容易出现意想不到的情况。
对于互斥锁和读写锁来说:
互斥锁(pthread_mutex_t
)和读写锁(pthread_rwlock_t
)需要我们手动在线程间的共享内存区域手动定义变量,以便多个线程可以访问。
定义了相关变量后,我们可以使用系统为我们提供的函数接口对其进行初始化,申请锁,释放锁,销毁锁等操作。
主要用于线程间的互斥访问共享资源。读写锁在读多写少的场景下,可以提高并发性能。
互斥锁和读写锁的生命周期与进程同步,在进程结束自动销毁,不需要我们进行手动释放。
posix匿名信号量简单认识
对于posix匿名信号量来说:
虽然名字和posix命名信号量有些像,但是功能和其有很大差别!!!posix匿名信号量主要用在线程间的同步,需要我们手动在线程间的共享内存区域中手动定义相关变量,生命周期与所属进程同步。使用流程和互斥锁,读写锁差不多。
互斥锁和读写锁专注于实现线程间互斥和读写控制,但是不能实现线程间执行顺序的同步。
posix匿名信号量提供了更广泛的同步能力,主要用于实现线程间执行顺序的同步。
POSIX匿名信号量常见函数
sem_init(sem_t *sem, int pshared, unsigned int value)
用于初始化匿名信号量。如果pshared
设置为0
,则信号量在线程间共享(适用于同一进程的多线程同步)。value
指定信号量的初始值。例如:sem_init(&sem, 0, 1); // 线程间共享,初始值为 1
sem_destroy(sem_t *sem)
用于销毁信号量,释放相关资源。在不再需要信号量时调用,例如:sem_destroy(&sem);
sem_wait(sem_t *sem)
用于等待信号量。当信号量的值大于0
时,sem_wait
会减少其值,并继续执行。如果信号量的值为0
,线程将阻塞,直到其他线程调用sem_post
释放信号量。例如:sem_wait(&sem); // 如果信号量值为 0,则阻塞等待
sem_trywait(sem_t *sem)
尝试获取信号量,但如果信号量当前值为0
,不会阻塞,而是直接返回-1
并设置errno
,适用于不想等待的场景。例如:if (sem_trywait(&sem) == 0) { // 成功获取信号量 } else { // 失败,信号量值为 0 }
sem_post(sem_t *sem)
用于释放信号量,使其他等待的线程可以继续执行。每调用一次sem_post
,信号量的值增加1
。例如:sem_post(&sem); // 释放信号量,唤醒等待的线程
sem_getvalue(sem_t *sem, int *sval)
获取信号量的当前值,并存储在sval
指向的变量中。例如:int value; sem_getvalue(&sem, &value); printf("Current semaphore value: %d\n", value);
POSIX匿名信号量使用流程
1.声明信号量
在全局或局部声明一个 sem_t 类型的变量:
sem_t sem;
2.初始化信号量
使用 sem_init 进行初始化,第二个参数设为 0 以支持线程间共享:
sem_init(&sem, 0, initial_value);
initial_value 设定信号量的初始值,决定了有多少个线程可以立即进入临界区。
3.线程操作信号量
等待信号量(P 操作):线程调用 sem_wait,如果信号量值大于 0,则减少其值并继续执行;否则阻塞等待:
sem_wait(&sem);
释放信号量(V 操作):线程完成任务后调用 sem_post,增加信号量值,允许其他线程继续:
sem_post(&sem);
线程执行任务 在获取信号量后,线程可以执行需要同步的任务,如访问共享资源、执行特定操作等。
4.销毁信号量
在程序退出或信号量不再使用时,调用 sem_destroy 释放资源:
sem_destroy(&sem);
这个流程适用于 任意 使用匿名信号量进行线程同步的场景,比如生产者-消费者模式、互斥控制、信号量控制的多线程任务等。如果你有更具体的需求,比如进程间同步,可以考虑使用 具名信号量(sem_open) 或 共享内存 + 信号量。
匿名信号量函数接口详解
sem_init() -初始化信号量
函数作用:初始化一个匿名信号量。
头文件:#include <semaphore.h>
函数原型:int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
- sem:指向 sem_t 类型变量的指针,指向待初始化的信号量。
- pshared:指定信号量的共享范围。
如果 pshared 为 0,则信号量只能在同一进程的线程间共享。
如果 pshared 非 0,则信号量可以在不同进程间共享(需要将信号量放置在共享内存 区)。
通常匿名信号量只在控制线程之间同步的时候使用,所以这个参数设置为0。 - value:信号量的初始值,必须是非负数。
返回值:
- 成功时返回 0。
- 失败时返回 -1,并设置 errno。
注意:
匿名信号量一般都用于线程之间的同步,不用在进程之间。所以匿名信号量一般会定义成静态全局变量或者全局变量,来使得所有线程可见。
sem_wait() - 获取信号量
函数作用:尝试获取信号量。
头文件:#include <semaphore.h>
函数原型:int sem_wait(sem_t *sem);
参数:
sem:指向要操作的信号量的指针。
返回值:
- 成功时返回 0。
- 失败时返回 -1,并设置 errno。
注意:
- 如果信号量的值大于 0,则将其减 1 并立即返回;
- 如果信号量的值大等于0,调用线程将被阻塞,直到信号量的值大于 0。
sem_trywait() - 非阻塞获取信号量
函数作用:尝试获取信号量,但不会阻塞。
头文件:#include <semaphore.h>
函数原型:int sem_trywait(sem_t *sem);
参数:
sem:指向要操作的信号量的指针。
返回值:
- 成功时返回 0。
- 如果信号量不可用,返回 -1,并设置 errno 为 EAGAIN。
- 其他错误,返回-1,并设置errno。
注意:
- 如果信号量的值大于 0,则将其减 1 并立即返回 0;
- 如果信号量的值大等于0,不阻塞,立即返回 -1,并设置 errno 为 EAGAIN。
sem_post() - 释放信号量
函数作用:释放信号量,将其值加 1
头文件:#include <semaphore.h>
函数原型:int sem_post(sem_t *sem);
参数:
sem:指向要操作的信号量的指针。
返回值:
- 成功时返回 0。
- 失败时返回 -1,并设置 errno。
注意:
sem_post函数的作用是释放信号量,将其值加 1。如果此时有线程被阻塞在 sem_wait()
函数中,则唤醒其中一个线程。
sem_destroy() - 销毁信号量
函数作用:销毁一个匿名信号量。
头文件:#include <semaphore.h>
函数原型:int sem_destroy(sem_t *sem);
参数:
sem:是一个指向要销毁的信号量的指针。
返回值:
- 成功时返回 0。
- 失败时返回 -1,并设置 errno。
sem_getvalue() - 获取信号量值
函数作用:获取 信号量当前的值,并将其存储在 sval
指向的变量中
头文件:#include <semaphore.h>
函数原型:int sem_getvalue(sem_t *sem, int *sval);
参数:
- sem:指向已初始化的 sem_t 类型的信号量对象。
- sval:指向 int 类型的变量,用于存储信号量的当前值。
返回值:
- 成功时返回 0,并将信号量的值存入 sval。
- 失败时返回 -1,并设置 errno
POSIX匿名信号量例子
例子描述:
我们创建两个线程:
- 生产者线程:生成数据,并通知消费者。
- 消费者线程:等待数据可用后进行消费。
两者通过 匿名信号量 进行同步,确保消费者不会在数据准备好之前执行。
实验预期结果:
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define NUM_ITERATIONS 5 // 生产和消费的次数
sem_t sem; // 声明匿名信号量
int shared_data = 0; // 共享数据
// 生产者线程
void* producer(void* arg) {
for (int i = 0; i < NUM_ITERATIONS; i++) {
sleep(1); // 模拟生产过程
shared_data = i + 1;
printf("Producer: produced %d\n", shared_data);
sem_post(&sem); // 增加信号量,通知消费者
}
return NULL;
}
// 消费者线程
void* consumer(void* arg) {
for (int i = 0; i < NUM_ITERATIONS; i++) {
sem_wait(&sem); // 等待信号量
printf("Consumer: consumed %d\n", shared_data);
}
return NULL;
}
int main() {
pthread_t prod_thread, cons_thread;
// 初始化匿名信号量,第二个参数=0 代表线程内共享
if (sem_init(&sem, 0, 0) != 0) {
perror("sem_init failed");
exit(EXIT_FAILURE);
}
// 创建生产者和消费者线程
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);
// 等待线程完成
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);
// 销毁信号量
sem_destroy(&sem);
return 0;
}
代码解释
-
sem_init(&sem, 0, 0);
- 第一个参数:指向信号量的指针。
- 第二个参数:设为
0
,表示该信号量仅在线程间共享(适用于同一进程的线程同步)。 - 第三个参数:初始化值
0
,表示消费者必须等待生产者生产数据。
-
生产者 (
producer
线程)- 生产数据后调用
sem_post(&sem);
,增加信号量值,通知消费者数据可用。
- 生产数据后调用
-
消费者 (
consumer
线程)sem_wait(&sem);
,如果信号量值为0
,消费者线程阻塞等待,直到生产者sem_post
增加信号量值后,才能继续执行。
-
销毁信号量 (
sem_destroy(&sem);
)- 释放信号量资源,避免内存泄漏。