系统编程(进程通信--综合)

信号量机制

信号量(semaphore)也被成为信号灯。是在多线程环境下用来保证关键代码段不被并发调用,在进入一个关键代码段之前每个线程必须要获取一个信号量,一旦关键代码段执行完成了,拥有信号量的线程必须释放信号量。另外想进入关键代码段的线程必须等待直到拥有信号量的线程释放了信号量。为了完成以上这个过程,就需要创建一个信号量,并且将获取信号量和释放信号量的操作分别放置在关键代码段的开始和结束处。 例:多个线程都想执行 读取一个文件内容的操作,但同一时刻该操作只能满足一个线程,为了保护读取文件内容的有序操作,就可以使用信号量来保护这段关键代码。备注:信号量可以实现线程的同步和互斥,但大多数情况下用于线程间的同步。

PV原子操作的原理

前面所讲到的信号量也就是操作系统所用到的PV 原子操作,它广泛用于进程或线程之间的同步和互斥,信号量本身是一个非负整数计数器,而PV 原子操作就是对这个整数计数器信号量(sem)的操作P 操作: 一次P操作使信sem 减 1, 如果 sem>=0 进程或者线程具有公共资源的访问权;V 操作: 一次V操作使信sem 加 1, 如果 sem<0 进程或者线程就将阻塞,直到 sem>=0为止;

PV原子操作的原理-续

当一个进程或者线程需要访问关键代码段(临界区)时,它必须先执行 P操作原语,使信号量S减1,当完成对临界区访问后,执行V操作来释放占用的临界区,若信号量的初值为1,当一个进程或者线程执行 P操作 后,S变成0,可以访问临界区,在该进程或者线程没有执行V操作之前,其他进程或线程也想访问临界区,也必须执行P操作,从而S 就变成 -1,因此第二个进程或线程就会被阻塞,直到第一个退出临界区,这时S变为0,从而唤醒第二个进程或线程等待系统调度后再进入临界区,当第二个进程或线程访问完后,执行V操作,如果没有其他进程或线程申请进入,S变成初值1。也就是说: P(S) 操作表示进程(或线程) 申请进入临界区,而V(S) 操作表示进程(或线程) 退出临界区;

信号量用于同步和互斥的基本用法

当信号量用于互斥的情况: 几个进程(或线程)往往只设置一个信号量,且将信号量初值设为1。
在这里插入图片描述

典型应用(综合案例)

6个哲学家,5双筷子,怎么进餐?

1. 问题背景

哲学家就餐问题:是经典的并发问题,描述了一群哲学家围坐在圆桌旁,每两个哲学家之间有一根筷子。哲学家需要两根筷子才能就餐,但可能存在死锁或竞争条件。
简化版本:本代码假设只有5双筷子,6个哲学家,通过信号量确保同时最多只有5个哲学家可以尝试拿筷子。

2. 代码功能

每个哲学家线程尝试获取信号量(代表筷子),成功后打印就餐信息,然后释放信号量。
通过信号量控制,确保不会出现死锁或竞争条件。

3. 代码结构

全局变量
sem_t sem:信号量,用于控制哲学家对筷子的访问。
任务函数:task
参数:
args:哲学家的编号(从0到5)。
功能:
哲学家尝试获取信号量(sem_wait)。
如果成功,打印就餐信息。
释放信号量(sem_post)。
核心逻辑:
通过信号量确保同时最多只有5个哲学家可以尝试拿筷子。
主函数:main
初始化信号量:

<C>
sem_init(&sem, 0, 5);
信号量初始值为5,表示最多有5双筷子可用。
创建哲学家线程:

<C>
pthread_t ids[NUM];
for (int i = 0; i < NUM; i++) {
    pthread_create(&ids[i], NULL, task, (void *)(long)i);
}
创建6个哲学家线程,每个线程的哲学家编号分别为05。
等待线程结束:

<C>
for (int i = 0; i < NUM; i++) {
    pthread_join(ids[i], NULL);
}

等待所有哲学家线程执行完毕。
销毁信号量:

sem_destroy(&sem); #### 4. 信号量的作用 信号量初始值为5:表示最多有5双筷子可用。 sem_wait(&sem): 哲学家尝试获取筷子,如果信号量为0(筷子已被占用),则阻塞等待。 sem_post(&sem): 哲学家就餐完毕后释放信号量,表示筷子可用。 #### 5. 代码执行流程 主函数初始化信号量并创建6个哲学家线程。 每个哲学家线程尝试获取信号量: 如果信号量大于0(有筷子可用),哲学家成功获取筷子并打印就餐信息。 如果信号量为0(筷子被占用),哲学家阻塞等待。 哲学家就餐完毕后释放信号量,表示筷子可用。 主函数等待所有线程执行完毕并销毁信号量。 #### 6. 测试输出 每行输出表示一个哲学家成功获取筷子并就餐:
1个哲学家得到筷子并吃了一口饭
第2个哲学家得到筷子并吃了一口饭
第3个哲学家得到筷子并吃了一口饭
第4个哲学家得到筷子并吃了一口饭
第5个哲学家得到筷子并吃了一口饭
第6个哲学家得到筷子并吃了一口饭
7. 代码改进建议

更真实的哲学家问题:
当前代码只是一个简化版本,未模拟哲学家拿两根筷子的过程。
可以考虑为每个哲学家定义左右两根筷子,并使用多个信号量。
死锁风险:
如果信号量初始值设置为与筷子数量相同(6),可能会出现死锁(所有哲学家同时拿起一根筷子)。
需要确保信号量初始值小于筷子数量。
随机延时:
可以在哲学家就餐过程中加入随机延时,模拟实际就餐时间的差异。
错误处理:
增加对sem_init、pthread_create等函数的返回值检查。
8. 总结
这段代码通过信号量解决了哲学家就餐问题中的竞争条件和资源争用问题,确保同时最多只有5个哲学家可以尝试拿筷子。代码简洁明了,适合作为信号量和线程同步的入门示例。

#include "head.h"

// 六个哲学家五双筷子
// 宏定义
#define NUM 6
// 定义信号量变量方便再多个函数中使用

sem_t sem;

void *task(void *args)
{
    int i = (int)(long)args;
    // 等待信号量    P操作
    sem_wait(&sem);

    // 当前是哪个哲学家再用餐
    printf("第%d个哲学家得到筷子并吃了一口饭\n", (i + 1));

    // 解除信号量  V操作
    sem_post(&sem);
}

int main(int argc, char const *argv[])
{

    // 初始化信号量,默认值为5---5双筷子
    sem_init(&sem, 0, 5);

    pthread_t ids[NUM];
    for (int i = 0; i < NUM; i++)
    {
        pthread_create(&ids[i], NULL, task, (void *)(long)i);
    }

    // 等待所有线程执行结束
    for (int i = 0; i < NUM; i++)
    {
        pthread_join(ids[i], NULL);
    }

    sem_destroy(&sem);
    return 0;
}
#include "head.h"

// 六个哲学家五双筷子
// 宏定义
#define NUM 6
pthread_mutex_t mutex;
// 定义互斥锁变量方便再多个函数中使用

sem_t sem;

void *task(void *args)
{
    int i = (int)(long)args;
    // 等待互斥锁    P操作
    // sem_wait(&sem);
    pthread_mutex_lock(&mutex);

    // 当前是哪个哲学家再用餐
    printf("第%d个哲学家得到筷子并吃了一口饭\n", (i + 1));

    // 解除互斥锁  V操作
    // sem_post(&sem);
    pthread_mutex_unlock(&mutex);
}

int main(int argc, char const *argv[])
{

    // 初始化互斥锁,默认值为5---5双筷子
    sem_init(&sem, 0, 5);

    pthread_t ids[NUM];
    for (int i = 0; i < NUM; i++)
    {
        pthread_create(&ids[i], NULL, task, (void *)(long)i);
    }

    // 等待所有线程执行结束
    for (int i = 0; i < NUM; i++)
    {
        pthread_join(ids[i], NULL);
    }

    pthread_mutex_destroy(&mutex);
    return 0;
}

生产者,消费者问题。

#include "head.h"

#define FIFO_PATH "/tmp/fifo_pc"

// 商品名称的最大长度
#define SIZE 20
// 定义信号量的初始值
#define SEM_NUM 5
// 自定义类型用于传递数据
typedef struct
{
    sem_t *sme1;
    sem_t *sme2;
    pthread_mutex_t *mutex;
    int fd;
    time_t start;
} DATA;
// 生产者的任务函数
void *producter_fun(void *args)
{
    int num = 1;
    char buff[SIZE] = {0};
    // 将线程传递过来的数据解析
    DATA *data = (DATA *)args;

    char *goods[] = {"apple",
                     "orange",
                     "banaba",
                     "tomato",
                     "pototo",
                     "pear"};

    // 计算柔性数组的长度
    // int n = sizeof(goods) / sizeof(goods[0]);
    int n = sizeof(goods) / sizeof(char *);
    // time的返回值单位是一个秒
    int time1 = time(NULL) - data->start;
    while (time1 < 10)
    {
        // 信号量等待操作---P操作
        sem_wait(data->sme1);
        pthread_mutex_lock(data->mutex);

        // 随机数产生随机下标
        int i = rand() % n;
        sprintf(buff, "%s---%d\n", goods[i], num++);
        // 将生产的果蔬产品存放到管道中
        write(data->fd, buff, SIZE);
        // 测试代码
        printf("producter set+++++ %s\n", buff);

        pthread_mutex_unlock(data->mutex);
        // 信号量V操作
        sem_post(data->sme2);
        // 延时操作:模拟生产过程要一些时间
        usleep(1000);
    }
    return NULL;
}

void *customer_fun(void *args)
{
    DATA *data = (DATA *)args;
    char food_name[SIZE] = {0};

    int time1 = time(NULL) - data->start;
    while (time1 < 10)
    {
        // P操作
        sem_wait(data->sme2);

        // 加互斥锁
        pthread_mutex_lock(data->mutex);

        read(data->fd, food_name, SIZE);

        printf("customer get++++++++ %s\n", food_name);

        // 解除互斥锁
        pthread_mutex_unlock(data->mutex);
        // V操作
        sem_post(data->sme1);
        usleep(1000);
    }
    // return NULL;
}

int main(int argc, int const *argv[])
{
    // 定义两个线程的变量
    pthread_t producter;
    pthread_t customer;
    // 管道文件不存在,就调用mkfifo创建管道文件
    if (access(FIFO_PATH, F_OK) == -1)
    {
        // perror("mkfifo failed");
        //  管道文件存在,就调用mkfifo创建管道文件
        if (mkfifo(FIFO_PATH, 0777) == -1)

        {
            perror("mkfifo: ");
            exit(-1);
        }

        // return -1; // exit(-1);
    }

    // 设置随机数种子
    srand(time(NULL));
    // 定义信号量变量或者计数器
    sem_t sem1, sem2;
    // 定义互斥锁变量
    pthread_mutex_t mutex;
    // 给结构体成员赋值  定义结构体变量
    DATA data = {0};

    data.sme1 = &sem1;
    data.sme2 = &sem2;
    data.mutex = &mutex;

    data.start = time(NULL);
    // 以读写模式打开管道
    int fd = open(FIFO_PATH, O_RDWR);
    data.fd = fd;
    // 初始化信号量

    sem_init(&sem1, 0, SEM_NUM);

    sem_init(&sem2, 0, 0);

    pthread_mutex_init(&mutex, NULL);

    pthread_create(&producter, NULL, producter_fun, &data);
    pthread_create(&customer, NULL, customer_fun, &data);

    pthread_join(producter, NULL);

    pthread_join(customer, NULL);

    pthread_mutex_destroy(&mutex);
    sem_destroy(&sem1);
    sem_destroy(&sem2);
    // 关闭管道描述符
    close(fd);
    // 删除管道文件
    unlink(FIFO_PATH);
    return 0;
}

这段代码实现了一个生产者-消费者模型,通过命名管道(FIFO)、信号量和互斥锁实现了生产者和消费者之间的数据传递和同步。以下是详细分析:

1. 功能概述

生产者:
随机生成商品(果蔬名称)并将其写入命名管道。
使用信号量sme1和sme2控制生产者和消费者的同步。
使用互斥锁mutex确保对管道的互斥访问。
消费者:
从命名管道中读取商品并打印。
使用信号量sme1和sme2同步。
使用互斥锁mutex互斥访问管道。

2. 代码结构

全局常量
FIFO_PATH:命名管道的路径。
SIZE:商品名称的最大长度。
SEM_NUM:信号量sme1的初始值(管道容量)。
数据结构

typedef struct {
    sem_t *sme1;            // 信号量1:控制生产者的写入
    sem_t *sme2;            // 信号量2:控制消费者的读取
    pthread_mutex_t *mutex; // 互斥锁:确保对管道的互斥访问
    int fd;                 // 管道文件描述符
    time_t start;           // 程序启动时间
} DATA;

生产者任务函数:producter_fun
初始化:
定义商品数组goods,包含果蔬名称。
随机数种子初始化。
生产商品:
通过sem_wait(data->sme1)检查是否有空间写入管道。
通过pthread_mutex_lock(data->mutex)加锁。
生成随机商品并将其写入管道。
释放互斥锁并调用sem_post(data->sme2)通知消费者。
延时:
使用usleep(1000)模拟生产耗时。
退出条件:
程序运行时间超过10秒。
消费者任务函数:customer_fun
消费商品:
通过sem_wait(data->sme2)检查是否有可读数据。
通过pthread_mutex_lock(data->mutex)加锁。
从管道中读取数据并打印。
释放互斥锁并调用sem_post(data->sme1)通知生产者。
延时:
使用usleep(1000)模拟消费耗时。
退出条件:
程序运行时间超过10秒。
主函数 main
管道初始化:
检查管道文件是否存在,若不存在则创建。
随机数种子:
设置随机数种子rand()。
信号量和互斥锁初始化:
初始化信号量sme1(初始值为SEM_NUM)和sme2(初始值为0)。
初始化互斥锁mutex。
结构体赋值:
将信号量、互斥锁和管道文件描述符赋值给DATA结构体。
线程创建:
创建生产者和消费者线程。
线程同步:
使用pthread_join等待线程结束。
资源清理:
销毁互斥锁和信号量。
关闭管道文件描述符。
删除管道文件。

3. 信号量和互斥锁的作用

信号量 sme1:
初始值为SEM_NUM(5),表示管道最多容纳5个商品。
生产者每写入一个商品,sme1减1。
消费者每读取一个商品,sme1加1。
信号量 sme2:
初始值为0,表示管道中初始没有商品。
生产者每写入一个商品,sme2加1。
消费者每读取一个商品,sme2减1。
互斥锁 mutex:
确保生产者和消费者对管道的互斥访问。

4. 测试输出
生产者输出:
producter set+++++ apple---1
producter set+++++ orange---2
消费者输出:
customer get++++++++ apple---1
customer get++++++++ orange---2
5. 代码改进建议

错误处理:
增加对sem_init、pthread_mutex_init、open等函数的返回值检查。
动态商品生成:
通过配置文件或命令行参数动态指定商品列表。
定时机制优化:
使用更精确的定时器(如clock_gettime)替换time(NULL)。
线程安全:
确保所有共享资源的访问都受到互斥锁的保护。

6. 总结

这段代码实现了一个典型的生产者-消费者模型,通过信号量和互斥锁实现了线程间的同步与互斥,同时使用命名管道作为数据传递的通道。代码结构清晰,功能明确,适合作为多线程编程的参考案例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值