Linux线程同步(3)生产者与消费者、条件变量与信号量

本文介绍了生产者-消费者问题的经典并发编程模型,探讨了互斥锁、信号量和条件变量在解决数据共享和同步问题中的应用,以及如何避免死锁和饥饿现象。通过实例展示了使用链表和线程池实现的基本生产者-消费者模型,并讨论了不同同步机制的优缺点。
摘要由CSDN通过智能技术生成

1.生产者与消费者的概念

生产者与消费者(Producer-Consumer)问题,是一个经典的并发编程问题。在这个问题中,涉及到两类进程:生产者和消费者生产者负责生产数据,并将其放入一个缓冲区中消费者则从缓冲区中取出数据并消费

生产者和消费者通常是并发运行的,即它们可能在不同的时间点执行。生产者可能会在生产数据后立即放入缓冲区,而消费者可能会在稍后的时间点从缓冲区中取出数据。因此,需要一种机制来同步生产者和消费者的行为,以确保缓冲区不会溢出(即生产者不会在缓冲区满时继续生产数据)或下溢(即消费者不会在缓冲区空时尝试取出数据)。

生产者与消费者模型是一个经典的并发编程模型,用于解决多线程或进程间的数据共享和同步问题。该模型涉及两类对象:生产者和消费者,以及一个共享的数据缓冲区

生产者:负责生产数据,并将这些数据放入共享缓冲区中。生产者可以在任何时间点生产数据,并将其添加到缓冲区,供消费者使用。

消费者:负责从共享缓冲区中取出数据,并进行消费(或处理)。消费者可以在任何时间点从缓冲区中取出数据进行处理。

共享缓冲区:是一个中介,用于存储生产者生产的数据,供消费者使用。这个缓冲区通常是有限的,因此生产者和消费者需要一种机制来同步他们的行为,以防止缓冲区溢出(生产者生产的数据太多,消费者来不及消费)或下溢(消费者消费的数据太多,生产者来不及生产)。

生产者-消费者模型中的关键概念和原则包括:

  1. 互斥:生产者和消费者需要互斥地访问共享缓冲区,以防止数据竞争和不一致的状态。这通常通过使用互斥锁(mutex)或其他同步机制来实现。
  2. 同步:生产者和消费者之间的同步是确保数据正确流动的关键。生产者不能在缓冲区满时继续生产数据,而消费者也不能在缓冲区空时尝试取出数据。这可以通过使用信号量(semaphores)、条件变量(condition variables)或其他同步机制来实现。
  3. 死锁和饥饿:在生产者-消费者模型中,需要特别注意避免死锁和饥饿问题。死锁是指多个线程或进程互相等待对方释放资源,导致无法继续执行。饥饿是指某些线程或进程长时间得不到所需的资源,而其他线程或进程却能得到足够的资源。为了避免这些问题,需要合理设计同步机制,并确保资源的公平分配。

初步实现生产者和消费者模型:

使用链表的数据结构,生产者生产数据,即不断地创建新节点,然后添加到链表中

首先创建链表的数据结构,然后初始化一个头节点。

为了生产者和消费者需要互斥地访问共享缓冲区,那么创建一个互斥量 mutex

// 创建一个互斥量
pthread_mutex_t mutex;

//链表节点数据结构
struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

生产者采用头插法向链表中添加数据

//生产者生产数据
void * producer(void * arg) {

    // 不断的创建新的节点,添加到链表中
    while(1) {
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000; //随机值
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        pthread_mutex_unlock(&mutex);
        usleep(100);
    }

    return NULL;
}

消费者需要读取缓冲区中的数据,然后删除节点,在删除的时候,为了避免溢出,需要先判断链表中是否还有节点可以删除,即if(head != NULL) 

//消费者,取内容
void * customer(void * arg) {

    while(1) {
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;

        // 判断是否有数据
        if(head != NULL) {
            // 有数据
            head = head->next;
            printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);
            usleep(100);
        } else {
            // 没有数据
            pthread_mutex_unlock(&mutex);
        }
    }
    return  NULL;
}

整体代码中,创建5个生产者线程,和5个消费者线程。

/*
    生产者消费者模型(粗略的版本)
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

// 创建一个互斥量
pthread_mutex_t mutex;

struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;


//生产者读数据
void * producer(void * arg) {

    // 不断的创建新的节点,添加到链表中
    while(1) {
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000; //随机值
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        pthread_mutex_unlock(&mutex);
        usleep(100);
    }

    return NULL;
}

//消费者,取内容
void * customer(void * arg) {

    while(1) {
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;

        // 判断是否有数据
        if(head != NULL) {
            // 有数据
            head = head->next;
            printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);
            usleep(100);
        } else {
            // 没有数据
            pthread_mutex_unlock(&mutex);
        }
    }
    return  NULL;
}

int main() {

    pthread_mutex_init(&mutex, NULL);

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while(1) {
        sleep(10);
    }  //防止子线程还没有执行完,互斥量就释放了

    pthread_mutex_destroy(&mutex);

    pthread_exit(NULL);  //主线程退出

    return 0;
}

上述代码的执行没有什么问题。但是,对于消费者来说,如果一直没有数据,那么它抢占到资源之后,会一直加锁,判断,解锁。这样就会对资源造成浪费。如果没有数据,那就通知生产者生产。为了解决这个问题,下文引入两种机制。

2.条件变量

条件变量(Condition Variables)是线程同步的一种机制,它允许一个或多个线程等待某个条件成立,或者在某个条件成立后唤醒一个或多个等待的线程。条件变量通常与互斥锁(Mutex)一起使用,以避免竞争条件和保证线程安全。

条件变量的主要操作包括:

  1. 等待(Wait):线程调用条件变量的等待函数,释放已持有的互斥锁,并进入等待状态。此时,线程不会消耗CPU资源,直到被唤醒或超时。在等待期间,如果条件变量接收到唤醒信号,线程会重新获取互斥锁,并检查条件是否满足。
  2. 唤醒(Signal)或通知(Notify):当条件满足时,另一个线程会调用条件变量的唤醒函数,以唤醒一个或多个等待的线程。被唤醒的线程会重新获取互斥锁,并再次检查条件是否满足。

条件变量的使用需要注意以下几点:

  1. 条件变量互斥锁必须一起使用,互斥锁用于保护共享数据,条件变量用于同步线程。条件变量自己不能保证安全问题。
  2. 在调用等待函数之前,线程必须已经获取了互斥锁,因为调用等待函数需要释放锁。在等待函数返回后,线程会重新获取互斥锁。
  3. 条件变量只能唤醒等待的线程不能强制线程退出等待状态。如果条件不满足,线程会再次进入等待状态。
  4. 条件变量支持“唤醒一个”或“唤醒所有”等待线程的操作。唤醒线程后,它会重新获取当线程进入休眠状态时释放的锁。

条件变量的类型 pthread_cond_t
◼ int pthread_cond_init(pthread_cond_t *restrict cond, const
pthread_condattr_t *restrict attr);
◼ int pthread_cond_destroy(pthread_cond_t *cond);
◼ int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
◼ int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict
abstime);
◼ int pthread_cond_signal(pthread_cond_t *cond);
◼ int pthread_cond_broadcast(pthread_cond_t *cond);

使用条件变量修改上述代码:

/*
    条件变量的类型 pthread_cond_t
    int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
    int pthread_cond_destroy(pthread_cond_t *cond);
    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
        - 等待,调用了该函数,线程会阻塞。
    int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
        - 等待多长时间,调用了这个函数,线程会阻塞,直到指定的时间结束。
    int pthread_cond_signal(pthread_cond_t *cond);
        - 唤醒一个或者多个等待的线程
    int pthread_cond_broadcast(pthread_cond_t *cond);
        - 唤醒所有的等待的线程
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

// 创建一个互斥量
pthread_mutex_t mutex;
// 创建条件变量
pthread_cond_t cond;

struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {

    // 不断的创建新的节点,添加到链表中
    while(1) {
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        
        // 只要生产了一个,就通知消费者消费
        pthread_cond_signal(&cond);

        pthread_mutex_unlock(&mutex);
        usleep(100);
    }

    return NULL;
}

void * customer(void * arg) {

    while(1) {
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;
        // 判断是否有数据,否则会出现段错误
        if(head != NULL) {
            // 有数据,则消费
            head = head->next;
            printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);
            usleep(100);
        } else {
            // 没有数据,需要等待,当生产者生产之后,就不阻塞了
            // 当这个函数调用阻塞等待的时候,会对互斥锁进行解锁,否则生产者拿不到互斥锁。
            //解除阻塞时,重新加锁
            //当不阻塞的,继续向下执行,会重新加锁。
            pthread_cond_wait(&cond, &mutex);
            pthread_mutex_unlock(&mutex);
        }
    }
    return  NULL;
}

int main() {

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while(1) {
        sleep(10);
    }

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    pthread_exit(NULL);

    return 0;
}

3.信号量

信号量(Semaphore)是一种用于控制多个线程或进程对共享资源访问的同步机制。它可以看作是一个计数器,用于表示可用资源的数量。信号量的主要操作包括P操作(等待)和V操作(释放)。

P操作(Wait):当一个线程或进程需要访问共享资源时,它首先会执行P操作。这个操作会将信号量的值减1,表示一个资源被占用。如果信号量的值大于0,表示还有可用资源,线程或进程可以继续执行;如果信号量的值为0,表示没有可用资源,线程或进程将被阻塞,直到有资源可用

V操作(Signal):当一个线程或进程完成对共享资源的访问后,它会执行V操作。这个操作会将信号量的值加1,表示一个资源被释放。如果有其他线程或进程正在等待该资源(即被P操作阻塞),那么它们将被唤醒并继续执行。

信号量的主要特点包括:

  1. 计数器功能:信号量本质上是一个计数器,用于表示可用资源的数量。
  2. 互斥性:通过控制对共享资源的访问,信号量可以实现互斥性,确保同一时间只有一个线程或进程访问共享资源。
  3. 同步性:信号量可以用于同步多个线程或进程的操作,确保它们按照预定的顺序访问共享资源。

信号量的使用需要注意以下几点:

  1. 在多线程或多进程环境中,需要确保对信号量的操作是原子的,即在一个操作完成之前不会被其他线程或进程打断。这通常通过使用互斥锁或其他同步机制来实现。
  2. 在创建信号量时,需要指定初始值,表示可用资源的数量。这个值应该根据实际情况进行设置,以确保不会发生资源耗尽的情况。
  3. 需要避免死锁和饥饿问题。例如,可以使用“超时”机制来避免线程或进程无限期地等待资源。
 信号量的类型 sem_t
    int sem_init(sem_t *sem, int pshared, unsigned int value);
        - 初始化信号量
        - 参数:
            - sem : 信号量变量的地址
            - pshared : 0 用在线程间 ,非0 用在进程间
            - value : 信号量中的值,生产+1,消费-1

    int sem_destroy(sem_t *sem);
        - 释放资源

    int sem_wait(sem_t *sem);
        - 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞,直到大于0(post)

    int sem_trywait(sem_t *sem);

    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
    int sem_post(sem_t *sem);
        - 对信号量解锁,调用一次对信号量的值+1

    int sem_getvalue(sem_t *sem, int *sval);

    //假设资源数为8
    sem_t psem;
    sem_t csem;
    init(psem, 0, 8);  //生产者所拥有的资源数最开始为8
    init(csem, 0, 0);  //最开始的时候,没有可以供消费者消费的数据,因此为0

    producer() {
        sem_wait(&psem);  8-1=7
        sem_post(&csem)  //通知消费者消费 1
    }

    customer() {
        sem_wait(&csem); 0等 有则消费1-1=0
        sem_post(&psem)  //通知生产者 7+1
    }

总体代码:

/*
    信号量的类型 sem_t
    int sem_init(sem_t *sem, int pshared, unsigned int value);
        - 初始化信号量
        - 参数:
            - sem : 信号量变量的地址
            - pshared : 0 用在线程间 ,非0 用在进程间
            - value : 信号量中的值,生产+1,消费-1

    int sem_destroy(sem_t *sem);
        - 释放资源

    int sem_wait(sem_t *sem);
        - 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞,直到大于0(post)

    int sem_trywait(sem_t *sem);

    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
    int sem_post(sem_t *sem);
        - 对信号量解锁,调用一次对信号量的值+1

    int sem_getvalue(sem_t *sem, int *sval);

    sem_t psem;
    sem_t csem;
    init(psem, 0, 8);
    init(csem, 0, 0);

    producer() {
        sem_wait(&psem);  8-1=7
        sem_post(&csem)  //通知消费者消费 1
    }

    customer() {
        sem_wait(&csem); 0等 1-1=0
        sem_post(&psem)  //通知生产者 7+1
    }

*/

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>

// 创建一个互斥量
pthread_mutex_t mutex;
// 创建两个信号量
sem_t psem;
sem_t csem;

struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {

    // 不断的创建新的节点,添加到链表中
    while(1) {
        sem_wait(&psem);
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        pthread_mutex_unlock(&mutex);
        sem_post(&csem);
    }

    return NULL;
}

void * customer(void * arg) {

    while(1) {
        sem_wait(&csem);  //0时等待,有的时候在消费,不用等待了
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;
        head = head->next;
        printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
        free(tmp);
        pthread_mutex_unlock(&mutex);
        sem_post(&psem);
       
    }
    return  NULL;
}

int main() {

    pthread_mutex_init(&mutex, NULL);
    sem_init(&psem, 0, 8);
    sem_init(&csem, 0, 0);

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while(1) {
        sleep(10);
    }

    pthread_mutex_destroy(&mutex);

    pthread_exit(NULL);

    return 0;
}

问题:

为何信号量和互斥锁调换位置就会出现死锁呢?

假设一直都是生产者线程抢占到cpu已经生产了8个(我们设置的psem为8),然后接下来又是生产者抢占到cpu,然后对互斥量上锁,在sem_wait函数阻塞,阻塞后cpu空闲,cpu去执行消费者线程,而消费者需要拿锁,但是锁在上一个生产者的手中,导致消费者卡在拿锁的函数,形成死锁。

生产者的psem=0,在wait阻塞,消费者拿不到锁产生死锁
消费者的csem=0,在wait阻塞,生产者拿不到锁产生死锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值