【系统编程】信号量与并发控制入门

一、概述

1.1 基本概念

信号量与信号无关
信号量相当于初始值为 N 的互斥量。N 值,表示可以同时访问共享数据的线程数

  • 本质
    信号量(sem_t)是一个非负整数计数器,用于控制多个线程或进程对公共资源的访问,保证同步和互斥。

  • 作用

    • 互斥:一次只允许一个线程或进程访问资源(类似互斥锁)。
    • 同步:按一定顺序协调线程或进程的执行(例如生产者–消费者模型)。
  • PV 操作

    • P 操作sem_wait):信号量 -1,如果结果 < 0 则阻塞等待
    • V 操作sem_post):信号量 +1,并唤醒等待线程
  • 访问判断

    • 信号量值 > 0:可直接获取资源(执行 P 操作)。
    • 信号量值 = 0:资源不可用,请求会阻塞(除非使用非阻塞方式)。

在这里插入图片描述

1.2 数据类型

#include <semaphore.h>
sem_t sem;   // 信号量变量
  • sem_t 是信号量类型
  • 值不能小于 0

二、主要 API

这些函数都在 <semaphore.h> 中声明,成功返回 0,失败返回 -1 并设置 errno(没有 pthread_ 前缀)。
编译时指定线程库 -pthread

2.1 初始化与销毁

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数

  • sem:信号量变量地址。
  • pshared
    • 0:线程间共享(常用)。
    • 非 0 :进程间共享(必须sem 放在可被多个进程映射访问的共享内存(如 mmap)中)。
  • value:信号量的初始值,N 值,代表同事访问共享数据的个数。决定并发数;1 类似互斥锁。
int sem_destroy(sem_t *sem);
  • 销毁信号量(在不再使用时释放资源)。

2.2 P 操作(获取资源 / 减 1)

int sem_wait(sem_t *sem);             // 阻塞方式
int sem_trywait(sem_t *sem);          // 非阻塞方式
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 限时阻塞

说明

  • sem_wait:如果信号量为 0,阻塞直到可用。
  • sem_trywait:立即返回,不阻塞。若不可用返回失败。
  • sem_timedwait:阻塞到超时时间(绝对时间)为止。
  • 一次调用做一次 -- 操作。当信号量的值为0时,再次 -- 信号量会阻塞。

时间结构 timespec

struct timespec {
    time_t tv_sec;   // 秒
    long   tv_nsec;  // 纳秒
};

示例(定时 1 秒):

time_t cur = time(NULL);
struct timespec t;
t.tv_sec = cur + 1;
t.tv_nsec = 0;
sem_timedwait(&sem, &t);

2.3 V 操作(释放资源 / 加 1)

int sem_post(sem_t *sem);
  • 作用
    • 信号量值 +1。
    • 如果有线程阻塞在 sem_wait 上,则唤醒一个。
    • 一次调用做一次 ++ 操作。当信号量的值为 N 时,再次 ++ 信号量会阻塞。

2.4 获取当前信号量值

int sem_getvalue(sem_t *sem, int *sval);
  • 作用
    • 将当前信号量值存入 *sval
  • 用途
    • 调试或监控信号量状态。
    • 不建议依赖此值做同步判断(存在竞态)。

三、生产者消费者模型

在这里插入图片描述

  • 有一个固定大小的仓库(这里用 queue[NUM] 的环形队列模拟)。
  • 生产者负责往仓库放产品。
  • 消费者负责从仓库取产品。
  • 规则:
    1. 仓库满了,生产者必须等(不能生产)。
    2. 仓库空了,消费者必须等(不能消费)。

问题是:如何让生产者和消费者在多线程环境下安全且高效地协作,而不会出现超产、空取或者数据错乱。

两个信号量解决同步问题:

  1. blank_num(空位数)
    • 初始值 = 仓库容量 NUM
    • 生产前 sem_wait(blank_num):没空位就阻塞。
    • 消费后 sem_post(blank_num):释放空位给生产者。
  2. product_num(产品数)
    • 初始值 = 0
    • 消费前 sem_wait(product_num):没产品就阻塞。
    • 生产后 sem_post(product_num):通知消费者有新货。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <semaphore.h>
#include <pthread.h>

#define NUM 10

int queue[NUM];                 // 全局数组实现环形队列
sem_t blank_num, product_num;   // 空格信号量,产品信号量

void *producer(void *arg)
{
    int i = 0;

    while (1) {
        sem_wait(&blank_num);                       // 生产者将空格数--,为0则阻塞等待
        queue[i] = rand() % 1000 + 1;               // 生产一个产品
        printf("-----Produce----%d\n", queue[i]);
        sem_post(&product_num);                     // 将产品数++

        i = (i + 1) % NUM;                          // 借助下标实现环形
        sleep(rand() % 3);
    }
}

void *consumer(void *arg)
{
    int i = 0;

    while (1) {
        sem_wait(&product_num);
        printf("----Consume----%d\n", queue[i]);
        queue[i] = 0;
        sem_post(&blank_num);

        i = (i+1) % NUM;
        sleep(rand()%3);
    }
}

int main(int argc, char *argv[])
{
    pthread_t pid, cid;

    sem_init(&blank_num, 0, NUM);
    sem_init(&product_num, 0, 0);

    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid, NULL, consumer, NULL);

    pthread_join(pid, NULL);
    pthread_join(cid, NULL);

    sem_destroy(&blank_num);
    sem_destroy(&product_num);

    return 0;
}
  • 生产者:往缓冲区(queue)放数据。
  • 消费者:从缓冲区取数据。
  • 信号量控制
    • blank_num:当前可用空位的数量(初始值 = 缓冲区大小)。
    • product_num:当前已有产品的数量(初始值 = 0)。
  • 环形队列:用数组下标 (i+1)%NUM 实现,避免移动数据。

四、哲学家就餐问题

  • 经典哲学家问题的死锁场景:

    • 5 个哲学家同时拿起左边的筷子。
    • 所有人都在等待右边筷子导致永久阻塞。
  • 解决方案

    • 资源顺序法:最后一个哲学家反向拿筷子(右手→左手),打破循环等待条件。
    • 这样最多有 4 个哲学家能同时持有一根筷子,至少有一根可用。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>

#define N 5  // 哲学家/筷子数量

pthread_mutex_t chopstick[N]; // 筷子互斥锁

void think(int id) {
    printf("哲学家 %d 正在思考...\n", id);
    sleep(rand() % 3 + 1);
}

void eat(int id) {
    printf("哲学家 %d 正在吃面...\n", id);
    sleep(rand() % 2 + 1);
}

void* philosopher(void* arg) {
    int id = *(int*)arg;
    int left = id;              // 左手筷子编号
    int right = (id + 1) % N;   // 右手筷子编号

    while (1) {
        think(id); // 思考

        // 为了防止死锁:最后一个哲学家先拿右手,再拿左手
        if (id == N - 1) {
            pthread_mutex_lock(&chopstick[right]);
            pthread_mutex_lock(&chopstick[left]);
        } else {
            pthread_mutex_lock(&chopstick[left]);
            pthread_mutex_lock(&chopstick[right]);
        }

        eat(id); // 吃面

        // 放下筷子
        pthread_mutex_unlock(&chopstick[left]);
        pthread_mutex_unlock(&chopstick[right]);
    }
    return NULL;
}

int main() {
    pthread_t tid[N];
    int id[N];
    srand(time(NULL));

    // 初始化筷子
    for (int i = 0; i < N; i++) {
        pthread_mutex_init(&chopstick[i], NULL);
    }

    // 创建哲学家线程
    for (int i = 0; i < N; i++) {
        id[i] = i;
        pthread_create(&tid[i], NULL, philosopher, &id[i]);
    }

    // 等待线程结束(实际上是死循环,不会结束)
    for (int i = 0; i < N; i++) {
        pthread_join(tid[i], NULL);
    }

    // 销毁互斥锁
    for (int i = 0; i < N; i++) {
        pthread_mutex_destroy(&chopstick[i]);
    }

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值