• Linux线程同步与互斥


    Liunx线程同步与互斥

    Linux线程互斥

    进程线程间的互斥相关背景的概念
    • 临界资源:凡是被线程共享访问的资源都是临界资源(多线程、多进程打印数据到显示器[临界资源]
    • 临界区:我的代码中访问临界资源的代码(在我的代码中,不是所有的代码都是进行访问临界资源的,而访问临界资源的区域我们称之为临界区
    • 互斥:在任意时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
    • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
    互斥量mutex
    • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量
    • 但有的时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交换
    • 多个线程并发的操作共享变量,会带来一些问题

    假如我们现在有一个抢票的场景:票是一个全局变量,然后现在我们创建了5个线程来抢票,那么在这个场景下会发生什么问题呢,下面我们来看一下代码

    #include<iostream>
    #include<pthread.h>
    #include<unistd.h>
    
    using namespace std;
    
    //抢票逻辑,1000张票,5个线程同时在抢
    
    int tickets = 1000;
    
    void* ThreadRoutine(void* args)
    {
        int id = *(int*)args;
        delete (int*)args;
        
        while(true)
        {
            //还有票
            if(tickets>0)
            {
                //微秒
                usleep(10000);
                cout<< "我是[" << id <<" ] 我要抢的票是: "<<tickets--<<endl;
            }
            //没有票
            else
            {
                break;
            }
        }
    }
    
    int main()
    {
        pthread_t tid[5];
        for(int i = 0;i<5;i++)
        {
            int* id = new int(i);
            pthread_create(tid+1,nullptr,ThreadRoutine,id);
        }
        
        for(int i = 0;i<5;i++)
        {
            pthread_join(tid[i],nullptr);
        }
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    运行结果:

    在这里插入图片描述

    我们发现这里只有总共1000张票,按理说抢到1,应该就停止抢票了,但是现在票变成了负数,线程却还在抢。 这种情况会导致在同一张票卖出去了几次,在我们实际购票的时候是绝对不允许这种情况出现的。

    大家现在可能心里会有一个问题:为什么我们上面的代码没有达到我们预期的结果呢?

    原因有以下几点:

    • if语句判断条件为真后,代码可以并发的切换到其他线程
    • usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
    • tickets–操作本身就不是一个原子操作

    这个时候可能就会有同学问了:为什么tickets–不是原子操作呢?

    我们对一个变量进行–操作,我们其实需要执行以下三步:

    1. load:将共享变量tickets从内存加载到寄存器中
    2. update:更新寄存器里面的值,执行-1操作
    3. store:将现在寄存器里面tickets的值从寄存器写回到共享变量tickets的内存地址

    在这里插入图片描述

    tickets–操作的汇编代码如下:

    movl  ticket(%rip), %eax     # 把ticket的值加载到eax寄存器中                                   
    subl  $1, %eax               # 把eax寄存器中的值减1
    movl  %eax, ticket(%rip)     # 把现在eax寄存器中tickets的值写回到tickets的内存地址
    
    • 1
    • 2
    • 3

    因为我们的–操作需要三个步骤才能完成,那么就有可能出现这种情况:

    一个线程刚刚把tickets的值加载进内存中,它就被切走了,我们知道线程切走会保存它的上下文信息。此时又有一个线程进来,因为第一个线程执行完第一步就被切走了,因此第二个线程看到的tickets还是1000,此时第二个线程一个人抢了5张票然后tickets变成了995.此时我们第一个线程又被CPU调度,因为保存了它的上下文信息,此时它就认为还有1000张票(但实际上只剩995张票了)然后它一个人抢了2张票,此时它会把最新的tickets(998)写回到内存,于是我们的票是本来是剩余995张,但是现在却由995->998这样就会导致我们上面一张票卖出了几次的问题。

    因此我们就可以知道对一个变量进行++或者–操作它不是原子的。

    那么问题来了:如何解决上面的问题呢?

    要解决上面的问题,需要做到以下三点:

    • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
    • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
    • 如果线程不在临界区中执行,那么该线程不能阻止其他线程s进入临界区。

    要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量

    在这里插入图片描述

    互斥量的接口

    初始化互斥量

    初始化互斥量有以下两种方法:

    • 静态分配

      pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 
      
      • 1
    • 动态分配

    初始化互斥量的函数叫做prhead_mutex_init

    函数原型如下:

    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
    
    • 1

    参数说明:

    • mutex:要初始化的互斥量
    • attr:要初始化的互斥量的属性,我们一般设置NULL即可

    返回值:

    • 互斥量初始化成功返回0,失败返回错误码

    销毁互斥量

    销毁互斥量的函数叫做pthread_mutex_destroy

    函数原型如下:

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    
    • 1

    参数说明:

    • mutex:要销毁的互斥量

    返回值:

    • 互斥量销毁成功返回0,失败返回错误码

    注意:

    • 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
    • 不要销毁一个已经加锁的互斥量
    • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

    互斥量加锁

    互斥量加锁的函数为pthread_mutex_lock

    函数原型如下:

    int pthread_mutex_lock(pthread_mutex_t *mutex); 
    
    • 1

    参数说明:

    • mutex:要加锁的互斥量

    返回值:

    • 加锁成功返回0,失败返回错误码

    在调用pthead_lock时,我们可能会遇到以下情况:

    • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
    • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

    互斥量解锁

    互斥量加锁的函数为pthread_mutex_unlock

    函数原型如下:

    int pthread_mutex_unlock(pthread_mutex_t *mutex); 
    
    • 1

    参数说明:

    • mutex:要解锁的互斥量

    返回值:

    • 解锁成功返回0,失败返回错误码

    我们可以看到互斥量的接口有一个特点——都是以pthead_mutex开头的后面再加上这个接口的功能的英文名,比如说初始化就加_Init,销毁就加上_destroy

    下面我们使用这些互斥量的这些接口来改进一下上面的抢票代码吧

    #include<iostream>
    #include<pthread.h>
    #include<unistd.h>
    
    using namespace std;
    
    //抢票逻辑,1000张票,5个线程同时在抢
    
    //定义一把互斥锁
    pthread_mutex_t mtx;
    int tickets = 1000;
    
    void* ThreadRoutine(void* args)
    {
        int id = *(int*)args;
        delete (int*)args;
        
        while(true)
        {
             //微秒
             usleep(5000);//我们这里改了一下位置,否则放里面的话有可能会导致一个线程抢了很多张票
            //加锁
            pthread_mutex_lock(&mtx);
            //还有票
            if(tickets>0)
            {
                //微秒
                usleep(10000);
                cout<< "我是[" << id <<" ] 我要抢的票是: "<<tickets--<<endl;
                //解锁
                pthread_mutex_unlock(&mtx);
            }
            //没有票
            else
            {
                //解锁
                pthread_mutex_unlock(&mtx);
                break;
            }
        }
    }
    
    int main()
    {
        //对锁进行初始化
        pthread_mutex_init(&mtx,nullptr);
        pthread_t tid[5];
        for(int i = 0;i<5;i++)
        {
            int* id = new int(i);
            pthread_create(tid+1,nullptr,ThreadRoutine,id);
        }
        
        for(int i = 0;i<5;i++)
        {
            pthread_join(tid[i],nullptr);
        }
        //用完了销毁锁
        pthread_mutex_destroy(&mtx);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    运行结果:

    在这里插入图片描述

    可以看到使用了互斥量之后,这里的抢票抢到1就不会再抢了,达到了我们的预期效果。

    注意:

    • 我们加锁之后,代码的执行效率一般会降低,这是为什么呢?

      这是因为在大部分情况下,加锁本身都是有损于性能的事,它会让多执行流由并行执行变为了串行执行,这几乎是不可避免的。

    • 代码是程序员写的!为了保证临界区的安全,必须保证每个线程都遵守相同的编码规范(A线程申请锁,其他线程的代码也必须申请锁)

    • 我们的线程在访问临界区的代码之前都必须要先申请锁,那也就意味着所有的线程必须看到同一把锁,那锁是不是临界资源呢?

      锁本身就是临界资源,我们通过锁去保护临界区, 那我们的锁需不需要保护自身的安全呢?

      答案是当然需要的,这就好比你想保护一个女生,如果你自己都保护不了自己,那你就不可能去保护那个女生。

      因此我们需要保证我们的锁本身是原子性的,不能够出现中间态。

    互斥量的实现原理
    • 经过上面的例子,大家已经意识到了i++或者++i都不是原子的,有可能会由数据一致性问题
    • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

    下面我们来看一下lock和unlock的伪代码:

    lock:
    	movb $0, %a1     
    	xchgb %a1, mutex 
    	if (%a1 > 0)
    		return 0;
    	else
    		挂起等待;
    	goto lock;
    unlock:
    	movb $1 mutex  	
    	唤醒等待的线程;
    	return 0;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们假设这里的mutex的初始值为1,然后a1是一个寄存器,当线程去申请锁时,需要执行以下几步:

    1. 线程将a1寄存器的值置为0,然后再将a1寄存器里面的值与mutex值进行交换
    2. 交换完成之后,对a1寄存器里面的值进行判断,如果值大于0,则申请锁成功,如果a1寄存器里面的值小于0则需要挂起等待
    3. 申请到锁的线程快执行完自己的任务时,释放刚刚申请到的锁,然后刚刚由于竞争锁失败的线程会被唤醒重新去竞争锁资源。

    下面我们通过图片来展示一下多个线程申请锁的过程:

    现在有两个线程,分别叫做线程A、线程B,线程A与线程B刚开始都将a1寄存器的值置为0

    在这里插入图片描述

    此时线程A先将a1寄存器里面的值与mutex值交换,线程B再将a1寄存器里面的值与mutex值进行交换,因为A先将寄存器里面的值与mutex值交换了,所以等B进行交换的时候内存里面mutex里面的值已经变成0了,因此B交换后a1寄存器里面的值和内存里面mutex的值都是0.

    在这里插入图片描述

    假如此时线程A的时间片到了,然后线程A会被切走(被切走的时候线程A的上下文信息会被保存起来),此时线程B去申请锁,因为此时线程B它a1寄存器里面的值是小于0的,因此它申请不到锁所以他要挂起等待

    在这里插入图片描述

    此时CPU开始调度线程A继续执行,此时线程A申请锁,因为此时线程A它a1寄存器里面的值是大于0的,因此它能够申请到锁

    在这里插入图片描述

    当线程A快执行完自己的认为时,它释放刚刚申请到的锁,此时内存中的mutex值会被置成1,刚刚由于竞争锁失败而挂起等待的线程B此时会被唤醒然后去重新竞争锁。

    以上就是互斥量的实现原理了,了解了上面的实现原理之后我们也就明白了,为什么当一个线程它申请到锁之后,即使它被切走了,但是其他的线程还是申请不到锁,这是因为即使它被切走了但是因为它的寄存器里面的值为1,所以也就相当于它是拿着锁走的,别人是拿不到这把锁的。

    可重入VS线程安全

    概念
    • 线程安全: 多个线程并发执行同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
    • 重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
    常见的线程不安全的情况
    • 不保护共享变量的函数
    • 函数状态随着被调用,状态发生变化的函数
    • 返回指向静态变量指针的函数
    • 返回线程不安全函数的函数
    常见的线程安全的情况
    • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
    • 类或者接口对于线程来说都是原子操作
    • 多个线程之间的切换不会导致该接口的执行结果存在二义性
    常见不可重入的情况
    • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
    • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
    • 可重入函数体内使用了静态的数据结构
    常见可重入的情况
    • 不使用全局变量或静态变量
    • 不使用用malloc或者new开辟出的空间
    • 不调用不可重入函数
    • 不返回静态或全局数据,所有数据都有函数的调用者提供
    • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
    可重入与线程安全联系
    • 函数是可重入的,那就是线程安全的
    • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
    • 果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
    可重入与线程安全的区别
    • 可重入函数是线程安全函数的一种
    • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
    • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

    常见锁概念

    死锁
    • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
    死锁的四个必要条件
    • 互斥条件: 一个资源每次只能被一个执行流使用
    • 请求与保持条件 一个执行流因请求资源而阻塞时,对已获得的资源保持不放
    • 不剥夺条件: 一个执行流已获得的资源,在未使用完之前,不能强行剥夺
    • 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系

    大家可能觉得死锁的四个必要条件不太好记,下面我来给大家讲一个小故事帮助大家记忆

    现在有两个小女孩,我们分别称他们为小女孩A与小女孩B,两个小女孩手上都只有5毛钱,但是买一根棒棒糖却要1块钱,这个时候A对B说:B啊,你把你手上的5毛钱给我吧,我去买一根棒棒糖。B对A说:A啊,你把你手上的5毛钱给我吧,我去买一根棒棒糖。这个时候A向B要她5毛钱,B向A要她的5毛钱,但是两个人都不肯把自己的5毛钱给对方,因为两个人都是好朋友所以不会出现直接抢夺对方5毛球的情况,因此这就形成了死锁。

    上面的这个小故事就体现了死锁的四个必要条件:

    A和B各自有5毛钱(互斥条件),两个人都向对方要对方的5毛钱,但是不肯将自己的5毛钱给对方(请求与保持条件),因为两个人是好朋友所以不会出现直接抢夺对方5毛钱的情况(不剥夺条件),于是两个人就形成一种头尾相接的循环等待资源的关系(循环等待条件

    避免死锁
    • 破坏死锁的四个必要条件
    • 加锁顺序一致
    • 避免锁未释放的场景
    • 资源一次性分配
    避免死锁的算法
    • 死锁检测算法
    • 银行家算法

    下面我们来看一段死锁的代码吧

    #include<iostream>
    #include<unistd.h>
    #include<pthread.h>
    
    using namespace std;
    
    pthread_mutex_t mtx1;//线程1的锁
    pthread_mutex_t mtx2;//线程2的锁
    
    void* Run1(void* arg)
    {
        char* msg = (char*)arg;
        //加锁
        pthread_mutex_lock(&mtx1);
        while(true)
        {
            cout<<"我是"<<msg<<endl;
            sleep(1);
            //申请对方的锁
            pthread_mutex_lock(&mtx2);
        }
        //解锁
        pthread_mutex_unlock(&mtx1);
    }
    
    void* Run2(void* arg)
    {
        char* msg = (char*)arg;
        //加锁
        pthread_mutex_lock(&mtx2);
        while(true)
        {
            cout<<"我是"<<msg<<endl;
            sleep(1);
            //申请对方的锁
            pthread_mutex_lock(&mtx1);
        }
        //解锁
        pthread_mutex_unlock(&mtx2);
    }
    
    int main()
    {
        pthread_t t1,t2;
        pthread_mutex_init(&mtx1,nullptr);
        pthread_mutex_init(&mtx2,nullptr);
        
        pthread_create(&t1,nullptr,Run1,(void*)"thread 1");
        pthread_create(&t2,nullptr,Run2,(void*)"thread 2");
        
        pthread_join(t1,nullptr);
        pthread_join(t2,nullptr);
        
        pthread_mutex_destroy(&mtx1);
        pthread_mutex_destroy(&mtx2);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    运行结果:

    在这里插入图片描述

    可以看到我们的代码运行起来之后死锁了。

    线程同步

    同步概念与竞条件
    • 同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
    • 竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件

    注意:

    • 首先需要明确的是,单纯的加锁是会存在一些问题的。比如说个别线程竞争力很强,每次都能够申请到锁,但是就是不办事,有可能会导致其它线程长时间竞争不到锁,从而引起饥饿问题
    • 单纯的加锁有没有错呢?没有错,它可以保证在同一时间只有一个线程进入临界区访问临界资源,但是它没有高效的让每一个线程使用这份临界资源。
    条件变量

    概念: 用来描述某种临界资源是否就绪的一种数据化描述

    条件变量通常需要配合mutex互斥锁一起使用

    条件变量函数

    初始化条件变量

    初始化条件变量的函数叫做——pthread_cond_init

    函数原型如下:

    int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
    
    • 1

    参数说明:

    • cond: 要初始化的条件变量
    • attr: 要初始化的条件变量的属性,我们一般设置为NULL即可

    返回值:

    • 初始化成功返回0,失败返回错误码

    销毁条件变量

    销毁条件变量的函数叫做——pthread_cond_destroy

    函数原型如下:

    int pthread_cond_destroy(pthread_cond_t *cond);
    
    • 1

    参数说明:

    • cond: 要销毁的条件变量

    返回值:

    • 条件变量销毁成功返回0,失败返回错误码

    等待条件满足

    等待条件变量满足的函数叫做pthread_cond_wait

    函数原型如下:

    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
    
    • 1

    参数说明:

    • cond: 要等待的条件变量
    • mutex: 当前线程所处临界区对应的互斥锁。

    返回值:

    • 调用成功返回0,失败返回错误码

    唤醒等待

    唤醒等待的函数有以下两个:

    int pthread_cond_broadcastpthread_cond_signal

    函数原型如下:

    int pthread_cond_broadcast(pthread_cond_t *cond);
    int pthread_cond_signal(pthread_cond_t *cond);
    
    • 1
    • 2

    参数说明:

    • cond: 唤醒在cond条件变量下等待的线程

    区别:

    • pthread_cond_signal函数用于唤醒等待队列中首个线程。
    • pthread_cond_broadcast函数用于唤醒等待队列中的全部线程。

    返回值:

    • 调用成功返回0,失败返回错误码

    注意:

    pthread_cond_broadcast和pthread_cond_signal

    前者是唤醒等待队列中所所有的线程,而后者只唤醒等待队列中的首个线程。前者会带来一个很不好的效应——惊群效应。多个线程同时被唤醒,但是最终只有一个线程能够获得“控制权”,其他获得控制权失败的线程可能重新进入休眠状态。等待获得控制权的线程释放锁资源后去通知下一个线程,这样就容易引起OS和CPU的管理调度负担,所以不建议使用。

    知道了条件变量的函数之后,下面我们来看一段线程同步的代码吧

    #include<iostream>
    #include<pthread.h>
    #include<unistd.h>
    #include<cstdio>
    
    using namespace std;
    
    pthread_mutex_t mtx;//锁
    pthread_cond_t cond;//条件变量
    
    void* Run(void* arg)
    {
        pthread_detach(pthread_self());
        cout<<(char*)arg<<" run..."<<endl;
        while(true)
        {
            pthread_cond_wait(&cond,&wait);//阻塞在这
            cout<<(char*)arg<<"活动..."<<endl;
        }
    }
    
    
    int main()
    {
        pthread_mutex_init(&mtx,nullptr);
        pthread_cond_init(&cond,nullptr);
        
        pthread_t t1,t2,t3;
        pthread_ctreate(&t1,nullptr,Run,(void*)"thread 1");
        pthread_ctreate(&t2,nullptr,Run,(void*)"thread 2");
        pthread_ctreate(&t3,nullptr,Run,(void*)"thread 3");
        while(true)
        {
            getchar();
            pthread_cond_signal(&cond);//唤醒一个线程
        }
        pthread_mutex_destroy(&mtx);
        pthread_cond_destroy(&cond);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    运行结果:

    在这里插入图片描述

    通过运行结果我们可以看到,我们唤醒的这三个线程是有顺序性的,主要是因为这几个线程启动时默认都会在该条件变量下去等待,而我们每次都唤醒的是在当前条件变量下等待的首个线程,当该线程执行完打印操作后会继续排到等待队列的尾部进行wait,所以我们能够唤醒的线程是有顺序性的。

    如果我们想每次唤醒在该条件变量下等待的所有线程,我们只需要将pthread_cond_signal函数改为pthread_cond_broadcast函数即可。

    #include<iostream>
    #include<pthread.h>
    #include<unistd.h>
    #include<cstdio>
    
    using namespace std;
    
    pthread_mutex_t mtx;//锁
    pthread_cond_t cond;//条件变量
    
    void* Run(void* arg)
    {
        pthread_detach(pthread_self());
        cout<<(char*)arg<<" run..."<<endl;
        while(true)
        {
            pthread_cond_wait(&cond,&wait);//阻塞在这
            cout<<(char*)arg<<"活动..."<<endl;
        }
    }
    
    
    int main()
    {
        pthread_mutex_init(&mtx,nullptr);
        pthread_cond_init(&cond,nullptr);
        
        pthread_t t1,t2,t3;
        pthread_ctreate(&t1,nullptr,Run,(void*)"thread 1");
        pthread_ctreate(&t2,nullptr,Run,(void*)"thread 2");
        pthread_ctreate(&t3,nullptr,Run,(void*)"thread 3");
        while(true)
        {
            getchar();
            //pthread_cond_signal(&cond);//唤醒一个线程
            pthread_cond_broadcast(&cond);//唤醒在该条件变量下等待的所有线程
        }
        pthread_mutex_destroy(&mtx);
        pthread_cond_destroy(&cond);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    运行结果

    在这里插入图片描述

    可以看到我们这一次的唤醒是一次唤醒了所有在cond条件变量下等待的线程。

    下面我们再来回答一个问题:

    为什么phread_cond_wait需要互斥量

    • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
    • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
    • 当某个线程进入临界区访问临界资源时需要先加锁,然后判断内部资源的情况,如果不满足当前线程的执行条件,那么当前线程需要在该条件变量下进行等待,但此时该线程是拿着锁被挂起的,这也就意味着这个锁再也不会被释放了,此时就会发生死锁问题。
    • 所以在调用pthread_cond_wait函数时,还需要将对应的互斥锁传入,此时当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁。
    • 当该线程被唤醒时,该线程会接着执行临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁,因此当某一个线程被唤醒时,实际会自动获得对应的互斥锁。

    注意:

    • 条件变量通常需要配合mutex互斥锁一起使用,其中条件变量是用来完成同步的,而互斥锁是用来完成互斥的。
    • pthread_cond_wait函数有两个功能,一是让线程在特定的条件变量下进行等待,二是让线程释放掉自己申请到的互斥锁。当该线程被唤醒后,该线程会立马获得之前释放的互斥锁,然后继续向下执行。
  • 相关阅读:
    23062day4
    【初识Jmeter】【接口自动化】
    .Net MVC 使用Areas后存在相同Controller时报错的解决办法; 从上下文获取请求的Area名及Controller名
    Linux服务器的性能监控与分析
    【嵌入式开发】UART
    基于32单片机的多功能电子语音时钟
    IDEA--tomcat日志乱码
    全网最透彻的Netty原理讲解 一
    彻底解决 IDC Incast
    MShadow中的表达式模板
  • 原文地址:https://blog.csdn.net/weixin_51885188/article/details/125395777