C语言中的并发与同步机制:互斥锁、条件变量、信号量与生产者消费者问题(三)

目录

一、信号量(Semaphores)

信号量概念

C语言中信号量的使用


一、信号量(Semaphores)

信号量概念

定义: 信号量是一种经典的同步原语,用于控制多个线程或进程对共享资源的访问。它是一个带有整数值的变量,通过特定的原子操作进行增加(称为“信号”或“发布”,如sem_post())和减少(称为“等待”或“获取”,如sem_wait())。信号量的值代表了可用资源的数量或访问权限的许可。

分类

  1. 计数型信号量:也称作通用信号量或计数信号量,其值可以取任意非负整数。它适用于管理一组可重入的资源,如数据库连接池或文件句柄池。计数型信号量的值表示资源池中当前可用资源的数量。

  2. 二值型信号量:又称为二进制信号量或互斥锁,其值只能为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,函数立即返回错误(通常为EAGAINEBUSY)。

    • 获取当前值(可选):某些实现可能提供如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)

    • 优点:既可以实现互斥访问(二值型),也能控制对多个资源的并发访问(计数型)。适用于资源池管理、多线程同步等多种场景。
    • 缺点:相比互斥锁,使用信号量可能导致更多的上下文切换,且如果使用不当,可能会导致线程饥饿或优先级反转等问题。
    • 适用场景:资源池管理、多线程同步、限制并发任务数、解决生产者-消费者问题等。

总结来说,互斥锁、条件变量和信号量各有优缺点,选择使用哪一种取决于具体的并发控制需求。互斥锁适用于简单的互斥访问,条件变量适用于复杂的线程间同步,而信号量则更通用,既可用于互斥,也可用于控制并发访问多个资源。在实际编程中,往往需要结合使用这些同步机制来构建高效且正确的并发程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

JJJ69

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

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

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

打赏作者

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

抵扣说明:

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

余额充值