【线程系列之四】信号量介绍

一、基本概念

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_opensem_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表示错误原因

注意事项:

  1. 多线程/多进程环境:在多线程或多进程环境中,信号量的值可能在 sem_getvalue() 调用后立即改变,因此该函数的结果可能并不总是可靠的,特别是在需要精确控制同步逻辑的场景中。
  2. 可移植性问题:尽管 sem_getvalue() 在许多遵循 POSIX 标准的系统上可用,但并非所有系统都保证会返回信号量的准确值。某些系统可能会因为性能考虑而返回一个估计值或特定值(如 0)。
  3. 用途限制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,表示一次只允许一个线程访问共享资源。我们创建了两个线程t1t2,它们都会尝试访问共享资源(在这个示例中,只是简单地打印一条消息)。每个线程在访问共享资源之前都会调用sem_wait()来等待信号量,访问完成后会调用sem_post()来释放信号量。这样,即使两个线程几乎同时运行,它们也不会同时访问共享资源,从而避免了数据竞争的问题。

运行结果为:
在这里插入图片描述

后续将针对不同的应用场景介绍信号量的使用。

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落淼喵_G

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值