当前位置: 首页 > article >正文

多线程详解1-互斥锁,读写锁,生产者消费者模型

文章目录

  • 互斥量mutex
    • 互斥量基本原理
    • 死锁
    • 代码实现
  • 读写锁
    • 基本概念
    • 为什么需要读写锁?
    • 相关函数
    • 读写锁实现
  • 生产-消费者模型
    • PV操作
    • 条件变量函数
    • 生产者消费者问题
    • 生产-消费者模型实现代码

互斥量mutex

互斥量基本原理

Linux系统编程 —互斥量mutex
互斥量mutex
前文提到,系统中如果存在资源共享,线程间存在竞争,并且没有合理的同步机制的话,会出现数据混乱的现象。为了实现同步机制,Linux中提供了多种方式,其中一种方式为互斥锁mutex(也称之为互斥量)。
互斥量的具体实现方式为:每个线程在对共享资源操作前都尝试先加锁,成功加锁后才可以对共享资源进行读写操作,操作结束后解锁。
互斥量不是为了消除竞争,实际上,资源还是共享的,线程间也还是竞争的,只不过通过这种“锁”机制就将共享资源的访问变成互斥操作,也就是说一个线程操作这个资源时,其它线程无法操作它,从而消除与时间有关的错误。

从互斥量的实现机制我们可以看出,同一时刻,只能有一个线程持有该锁。如果有同时有多个线程持有该锁,那就没有实际意义了。
但是,这种锁机制不是强制的,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。

因此,即使有了mutex,其它线程如果不按照这种锁机制来访问共享数据的话,依然会造成数据混乱。所以为了避免这种情况,所有访问该共享资源的线程必须采用相同的锁机制。
主要应用函数:

​ pthread_mutex_init函数
​ pthread_mutex_destroy函数
​ pthread_mutex_lock函数
​ pthread_mutex_trylock函数
​ pthread_mutex_unlock函数

以上5个函数的返回值都是:成功返回0,失败返回错误号。
在Linux环境下,类型pthread_mutex_t其本质是一个结构体。但是为了简化理解,应用时可忽略其实现细节,简单当成整数看待。mutex一般以下面方式定义:
pthread_mutex_t mutex;
变量mutex只有两种取值1、0。

pthread_mutex_trylock函数
函数原型:

int pthread_mutex_trylock(pthread_mutex_t *mutex);

函数作用:
对共享资源尝试加锁。它与pthread_mutex_lock函数的区别是,使用lock函数对共享资源进行加锁时,如果加锁不成功,则线程就阻塞;而如果使用trylock,则加锁不成功时不会阻塞当前线程,而是立即返回一个值来描述互斥锁的状况。

死锁

线程试图对同一个互斥量A加锁两次。
线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁
在这里插入图片描述

代码实现

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *thread_main(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&mutex);
        printf("HOUHOU\n");
        sleep(rand() % 3);
        printf("GUGU\n");
        pthread_mutex_unlock(&mutex);
        sleep(rand() % 3);

    }
}

int main()
{
    pthread_t tid;
    srand(time(NULL));

    pthread_create(&tid, NULL, thread_main, NULL);

    while(1)
    {
        pthread_mutex_lock(&mutex);
        printf("houhou\n");
        sleep(rand() % 3);
        printf("gugugu\n");
        pthread_mutex_unlock(&mutex);
        sleep(rand() % 3);

    }

    pthread_join(tid,NULL);

    return 0;
}

在这里插入图片描述
如果不加互斥锁,则会出现混乱的输出。
printf(“HOUHOU\n”);临界区不加锁,会失去cpu。
在这里插入图片描述

读写锁

基本概念

读写锁其实还是一种锁,是给一段临界区代码加锁,但是此加锁是在进行写操作的时候才会互斥,而在进行读的时候是可以共享的进行访问临界区的。
在这里插入图片描述

为什么需要读写锁?

有时候,在多线程中,有一些公共数据修改的机会比较少,而读的机会却是非常多的,此公共数据的操作基本都是读,如果每次操作都给此段代码加锁,太浪费时间了而且也很浪费资源,降低程序的效率,因为读操作不会修改数据,只是做一些查询,所以在读的时候不用给此段代码加锁,可以共享的访问,只有涉及到写的时候,互斥的访问就好了。

相关函数

(1)pthread_rwlock_init()—->初始化函数
功能:初始化读写锁
头文件:#include<pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthred_rwlockattr_t *restrict attr);

参数说明:
rwlock:是要进行初始化的
attr:是rwlock的属性
ps:此参数一般不关注,可设为NULL

(2)pthread_rwlock_destroy—->销毁函数
功能:销毁初始化的锁
头文件:#include<pthread.h>

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数说明:
rwlock:是需要进行销毁的锁

(3)加锁和解锁
在进行读操作的时候加的锁:
pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
在进行写操作的时候加的锁:

pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
对读/写统一进行解锁:
pthread_rwlock_unlock(pthread_rwlock_t* rwlock);

读写锁实现

#include<stdio.h>
#include<unistd.h>
#include<malloc.h>
#include<stdlib.h>
#include<pthread.h>
pthread_rwlock_t rwlock;//声明读写锁
int count;
//写者线程的入口函数
void*route_write(void*arg)
{
    int i=*(int*)arg;//i是写者线程的编号
    free(arg);
    while(1){
        int t=count;
        //加锁
        pthread_rwlock_wrlock(&rwlock);
        printf("route_write:%d,%#x,count=%d,++count=%d\n",i,\
                pthread_self(),t,++count);
        //解锁
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}
//读者线程的入口函数
void*route_read(void*arg)
{
    int i=*(int*)arg;//i是读者线程的编号
    free(arg);
    while(1){
        //加锁
        pthread_rwlock_rdlock(&rwlock);
        printf("route_read:%d,%#x,count=%d\n",i,pthread_self(),count);
        //解锁
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}

int main()
{
    int i=0;
    //初始化读写锁
    pthread_rwlock_init(&rwlock,NULL);
    pthread_t tid[8];
   
    //创建3个写者线程
    for(i=0;i<3;i++){
        int*p=(int*)malloc(sizeof(int));
        *p=i;
        pthread_create(&tid[i],NULL,route_write,(void*)p);
    }
    
    //创建5个读者线程
    for(i=0;i<5;i++){
        int*p=(int*)malloc(sizeof(int));
        *p=i;
        pthread_create(&tid[i+3],NULL,route_read,(void*)p);
    }
   
    //主线程等待新创建的线程
    for(i=0;i<8;i++)
        pthread_join(tid[i],NULL);
    //销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

在这里插入图片描述

生产-消费者模型

PV操作

明确定义
要理解生产消费者问题,首先应弄清PV操作的含义:PV操作是由P操作原语和V操作原语组成(原语是不可中断的过程),对信号量进行操作,具体定义如下:

P(S):①将信号量S的值减1,即S=S-1;

       ②如果S ,则该进程继续执行;否则该进程置为等待状态,排入等待队列。

V(S):①将信号量S的值加1,即S=S+1;

       ②如果S>0,则该进程继续执行;否则释放队列中第一个等待信号量的进程。

这只是书本的定义,对于这部分内容,老师先不要急于解释上面的程序流程,而是应该让学生首先知道P操作与V操作到底有什么作用。

P操作相当于申请资源,而V操作相当于释放资源。所以要学生记住以下几个关键字:

P操作—申请资源—pthread_cond_wait()

V操作—释放资源—pthread_cond_signal()

条件变量函数

 #include <pthread.h>
 
       //条件变量的销毁
       int pthread_cond_destroy(pthread_cond_t *cond);
       //条件变量的初识化
       int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);
              
       pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//静态声明

pthread_cond_t cond =PTHREAD_COND_INITIALIZER;,相当于调用函数pthread_cond_init()初始化,并且参数attr为NULL。

条件变量的操作函数

   #include <pthread.h>
 
   //唤醒该条件变量的所有线程
   int pthread_cond_broadcast(pthread_cond_t *cond);
   //唤醒该条件变量中的一个线程
   int pthread_cond_signal(pthread_cond_t *cond);
 
   int pthread_cond_timedwait(pthread_cond_t *restrict cond,
          pthread_mutex_t *restrict mutex,
          const struct timespec *restrict abstime);
          
   //让该线程在某个条件变量进行等待
   int pthread_cond_wait(pthread_cond_t *restrict cond,
          pthread_mutex_t *restrict mutex);

条件变量总是需要与互斥量结合使用,互斥量能限制一个线程能够访问共享资源,条件变量是在共享变量状态改变时发出通知。一个线程调用pthread_cond_wait函数则让它在一个条件变量下进行等待,pthead_cond_wait函数进行三个步骤:
释放互斥量
阻塞等待
当被唤醒时,重新获得互斥量并返回

生产者消费者问题

生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
在这里插入图片描述

生产-消费者模型实现代码

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

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

struct msg* head;
struct msg* mp;

pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;


void *consumer(void *arg)
{
    for(;;)
    {
        pthread_mutex_lock(&lock);
        while(head == NULL)   
        {
            pthread_cond_wait(&has_product,&lock);//pthread_cond_wait() 用于阻塞当前线程,等待别的线程使用	pthread_cond_signal()或pthread_cond_broadcast来唤醒它。
        }

        //链表不为空,则直接"消费"
        mp = head;
        head = mp->next;
        pthread_mutex_unlock(&lock);

        printf("Consume ---%d\n",mp->num);
        free(mp);
        sleep(rand()%5);
    }
}

void *producter(void *arg) 
{
    for(;;)
    {
        mp = malloc(sizeof(struct msg));
        mp->num = rand() % 1000 + 1;
        printf("Product ---%d\n",mp->num);

        pthread_mutex_lock(&lock);
        mp->next = head;
        head = mp;
        pthread_mutex_unlock(&lock);

        pthread_cond_signal(&has_product);
        sleep(rand() % 5);
    }

}

int main()
{
    pthread_t pid,cid;    
    srand(time(NULL));

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

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

    return 0;
}

在这里插入图片描述
消费者:关于while循环的目的是等待队列中有产品(数据)可供消费。

当线程进入这个循环时,它首先获取互斥锁lock,以确保在检查队列状态时不会被其他线程中断。
然后,它检查队列头指针head是否为空。如果队列为空,说明没有产品可供消费。
在这种情况下,线程调用pthread_cond_wait(&has_product, &lock),它会释放互斥锁lock并等待条件变量has_product的信号。
当其他线程向队列添加产品时,它们会发送信号给has_product,唤醒等待的消费者线程。
一旦有产品可供消费(即队列不再为空),线程会重新获取互斥锁并继续执行后续代码。

为什么使用while:
使用while而不是if的原因是防止虚假唤醒(spurious wakeups)。
虚假唤醒是指在没有明确信号的情况下,线程被唤醒。如果使用if,线程可能在没有产品的情况下被唤醒,然后错误地继续执行后续代码。
while循环会在每次被唤醒后重新检查条件,确保只有在队列非空时才继续执行。

总结:while循环用于等待队列中有产品可供消费,同时避免虚假唤醒的问题。


http://www.kler.cn/news/156654.html

相关文章:

  • docker 如何在容器内重启 php
  • 数据管理系统-week9-事务处理程序简介
  • ADAudit Plus:强大的网络安全卫士
  • RflySim | 姿态控制器设计实验一
  • 接口测试--知识问答
  • CCFCSP试题编号:202006-2试题名称:稀疏向量
  • 科普类软文怎么写才能提高用户接受度?媒介盒子分享
  • 拼多多关键词搜索商品列表接口调用演示,关键词搜索接口,item_search - 按关键字搜索商品列表案例
  • 在线陪诊系统: 医疗科技的崭新前沿
  • MacOS 14 系统 XCode15、 Flutter 开发 IOS
  • QJsonObject 是 Qt 框架中用于表示 JSON 对象的类
  • opencv知识库:cv2.add()函数和“+”号运算符
  • 计算机网络TCP篇③问答篇
  • 【NGINX--11】利用访问日志、错误日志和 请求跟踪进行调试和故障排除
  • Python与ArcGIS系列(十四)批量输出shp(自定义工具)
  • wordpress建站优化加速教程-Redis加速
  • 制作openeuler的livecd
  • scikit-learn线性回归法进行利润预测
  • Sui生态DeFi将参加Builder DAO举办的活动,为期三天畅谈如何Build on Sui
  • WT2605-24SS高品质录音语音芯片:实时输出、不保存本地,引领音频技术新潮流
  • 如何把栅格图像变平滑
  • Future.forEach异步遍历列表,Future.doWhile实现了一个无限循环,直到满足某个条件才停止
  • CGE模型应用
  • 【华为OD题库-058】矩阵中非1的元素个数-java
  • 知识点滴 - 什么是半透膜和渗透压
  • 计算机网络:传输层——多路复用与解复用
  • 十、FreeRTOS之FreeRTOS时间片调度
  • 实现简单的Spring
  • thinkphp6出现 htmlentities() expects parameter 1 to be string, array given
  • 金融帝国实验室(Capitalism Lab)官方正版游戏『2023双12特卖』