Linux 多线程之线程安全(同步与互斥/互斥锁/条件变量/死锁/)

目录

线程安全

线程同步与互斥

互斥锁(量)

        互斥锁接口

可重入函数&线程安全

死锁

条件变量

条件变量接口

条件变量使用规范

为什么pthread_cond_wait()中要传入互斥锁?

为什么互斥锁和条件变量要配合使用?


戳链接( ̄︶ ̄)↗Linux 多线程( 线程概念/特点/优缺点/与进程比较)

戳链接( ̄︶ ̄)↗Linux 多线程(线程控制(创建/终止/等待/分离))

线程安全

线程安全: 在多个执行流中对同一个临界资源进行操作访问, 而不会造成数据二义性 .

也就是说, 在拥有共享数据的多条线程并行执行的程序中, 通过同步和互斥机制保证各个线程都可以正常且正确的执行, 不会出现数据污染等意外情况, 也就是保证了线程安全 .

这样说可能不直观, 就拿买票举栗子, 当小公举开演唱会时, 门票的数量是一定的, 肯定是不够卖的, 但往往还有黄牛来倒票.....

就拿黄牛倒票举个栗子 . 假定还剩30张票, 5位黄牛要抢这30张票, 来看代码.

#include<iostream>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<pthread.h>
using namespace std;

int ticket = 30;//票数

void* route(void *arg){
    while(ticket > 0){
        usleep(100);
        --ticket;
        printf("第%d位黄牛抢到票, 还剩%d张\n", *(int*)arg, ticket);
    }
    return NULL;
}
int main(){
    pthread_t tid[5];
    int n[5] = {1,2,3,4,5};
    for(int i = 0; i < 5; ++i){
        int ret = pthread_create(&tid[i], NULL, route, n + i);
        if(ret){
            fprintf(stderr, "pthread_create:%s\n", strerror(ret));
        }
    }
    for(int i = 0; i < 5; ++i){
        pthread_join(tid[i], NULL);
    }
    return 0;
}

         

可以看到, 抢票的过程中出现了问题, 30张票抢完了, 还在抢. 这是为什么呢? 明明抢票的判断条件是ticket > 0啊, 怎么会出现这种情况呢?

这是因为, 当多个线程同时对一个临界资源访问时, 就有很大的可能会出现数据二义性的问题, 就比如上面的例子, 当还有最后一张票时, 第一个黄牛的线程判断ticket > 0 进入循环抢票, 但此时如果ticket还没有减一, 这个线程就被挂起等待, 操作系统给CPU调度了其他的黄牛线程, 此时其他线程判断ticket > 0, 也就入了抢票循环, 这就出问题了, 同一张票, 本该抢完就结束的, 但这样一来就会出现上面例子问题发生, 此时线程是不安全的.  总结一下就是

  • 1. while判断后, 可以并发的切换到其他线程
  • 2. usleep模拟一段业务过程, 在这段过程中, 可能会并发多个线程也运行到这段代码
  • 3. --ticket本身就不是一个原子操作
    如果调试来看的话,  --ticket一个语句要对应三条汇编指令

线程同步与互斥

现代计算机系统大都是多任务操作系统, 这种多任务的模式可能会产生一些问题, 比如各任务间 :

  • 访问/操作同一份资源. 
  • 不同任务间有依赖关系, 一个任务的完成需要另一个任务

上面抢票的例子就是一个很典型的多任务并发操作同一份资源时出现了问题, 这时就需要同步机制来控制协调, 保证其不会出问题.

同步 : 不同的任务(执行流) 间必须按照某种特定的次序来执行, 来保证多任务并发中的多任务处理的合理性

比如上面抢票例子中, 如果合理的话, 应该是这样, A线程在抢票, 其他线程必须等着,当A抢完之后, 通知其他线程可以抢票了, 这时其他线程才能抢票. 再举个例子, 是A任务的运行必须要B任务运行产生的数据, A就必须等着B运行完.) 重点理解, 在编程, 通信中所说的同步与我们在生活中理解的同步有差异. “同”字应是指协同, 协助, 互相配合. 其主旨在协同步调, 按预定的先后次序运行, 同步只保证资源访问的合理性.

互斥 : 同一时间只有一个任务(执行流)可以对临界资源进行访问,(一个执行流访问期间, 其他执行流不能访问).

来保证临界资源访问的安全性(互斥只保证对临界资源访问的安全性), 就拿上面抢票的例子来说, 票数ticket就是临界资源. 

再来看一些其他相关概念

临界资源 : 多个执行流共享各种资源, 然而有很多资源一次只能供一个执行流使用. 这样的资源称为临界资源. 在代码中多见于全局变量, 静态变量, 以及其他的一些共享资源.

临界区 : 代码中访问临界资源的代码称之为临界区.

原子操作/原子性 : 不会被任何调度机制打断的操作, 所以这种操作只有两种状态, 要么一次性操作结束, 要么还没有开始操作或是已经结束. 不会有操作到一半被调度机制打断的可能.

为了解决上面例子中的问题, 就引入了的概念, 来实现临界资源访问安全的锁, 就称之为互斥锁或互斥量.

互斥锁(量)

互斥量, 又称为互斥锁, 是一种保护临界区的特殊变量, 它可以处于上锁(锁定)状态(lock), 也可以处于解锁状态(unlock).

互斥锁是最简单也是最有效的线程同步机制.

  • 如果互斥锁是锁定的, 那么肯定有某个线程正在持有这个锁 .
  • 如果没有线程持有这个锁, 那么这个锁就处于解锁状态.
  • 唯一性 : 如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量.
  • 原子性 : 给互斥锁上锁的过程是一个原子操作, 保证一个线程在给一个互斥量上锁的过程中, 不会同时有其他线程也成功上锁.
  • 非繁忙等待:如果一个线程已经锁定了一个互斥量, 第二个线程又试图去锁定这个互斥量, 则第二个线程将被挂起 (不占用任何cpu资源), 直到第一个线程解除对这个互斥量的锁定为止, 第二个线程则被唤醒并继续执行, 同时第二个线程再锁定这个互斥量.
  • 每一个互斥锁内部都有一个等待队列, 用来保存等待该互斥锁的线程. 当互斥锁处于解锁状态时, 阻塞在等待队列中的线程就可以竞争这个锁, 竞争到锁的线程就去执行, 其他的线程继续阻塞在等待队列. 当互斥锁处于锁定状态时, 有线程想要获取锁, 就会阻被塞进入互斥锁的等待队列.
  • 互斥锁只能短时间的持有, 当临界资源访问完之后应立即释放

互斥锁接口

互斥量类型 : pthread_mutex_t 

互斥锁本质是一个结构体, 但我们在使用时无需关心内部结构, 我们只需要知道其内部实现有一个计数器, 当其为1时, 表示锁定状态, 为0时表示解锁状态.

初始化 

  • 静态初始化 : pthread_mutex_t  mutex  =  PTHREAD_MUTEX_INITIALIZER //不需要释放销毁(因为在栈上)
  • 动态初始化 : pthread_mutex_init( pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr)
                          //需要释放销毁(因为在堆上, 用完后需要用函数pthread_mutex_destroy来销毁)

       功能 : 初始化互斥锁变量
       参数 : mutex : 传入互斥锁变量的地址
                 attr : 互斥锁的属性, 通常传入NULL,传入NULL为默认属性(线程间共享)
                 restrict关键字 : 只用于限制指针, 告诉编译器, 所有修改该指针指向内存中内容的操作, 只能通过本指针完成. 不能
                                          通过除本指针以外的其他变量或指针修改.
       返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno 

       注意 : 必须在访问临界资源的进程创建之前就初始化

加锁 

原型 : pthread_mutex_lock( pthread_mutex_t* mutex)

功能 : 在访问临界资源之前加锁(阻塞操作, 不能加锁则阻塞, 进入等待队列)

参数 : mutex : 传入要加锁的互斥锁的地址

返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno

原型 : pthread_mutex_trylock( pthread_mutex_t* mutex)

功能 : 在访问临界资源之前尝试加锁 ( 非阻塞操作, 如果判断不能加锁, 则直接返回)

参数 : mutex : 传入要加锁的互斥锁的地址 

返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno

解锁  

原型 : pthread_mutex_unlock( pthread_mutex_t* mutex)

功能 : 在访问临界资源之后解锁

参数 : mutex : 传入要解锁的互斥锁的地址 

返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno

销毁

原型 : pthread_mutex_destroy( pthread_mutex_t* mutex)

功能 : 在互斥锁不再使用之后, 销毁释放资源(只针对init函数初始化的互斥锁)

参数 : mutex : 传入要销毁的互斥锁的地址 

返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno

注意 : 必须在访问临界资源的线程等待退出后, 再销毁init的互斥锁, 不能销毁还没有解锁的锁.

 来改进一下上面抢票例子中的代码.

#include<iostream>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<pthread.h>
using namespace std;

int ticket = 30;
pthread_mutex_t mutex;
void* route(void *arg){
    while(1){
        pthread_mutex_lock(&mutex);
        if(ticket > 0){
            usleep(100);
            --ticket;
            printf("第%d位黄牛抢到票, 还剩%d张\n", *(int*)arg, ticket);
            pthread_mutex_unlock(&mutex);
            usleep(10);
        }
        else{
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return NULL;
}
int main(){
    pthread_t tid[5];
    int n[5] = {1,2,3,4,5};
    pthread_mutex_init(&mutex, NULL);
    for(int i = 0; i < 5; ++i){
        int ret = pthread_create(&tid[i], NULL, route, n + i);
        if(ret){
            fprintf(stderr, "pthread_create:%s\n", strerror(ret));
        }
    }
    for(int i = 0; i < 5; ++i){
        pthread_join(tid[i], NULL);
    }
    pthread_mutex_destroy(&mutex);
    return 0;
}

可重入函数&线程安全

线程安全: 在多个执行流中对同一个临界资源进行操作访问, 而不会造成数据二义性 .

也就是说, 在拥有共享数据的多条线程并行执行的程序中, 通过同步和互斥保证各个线程都可以正常且正确的执行, 不会出现数据污染等意外情况, 也就是保证了线程安全 .

重入 : 同一个函数被不同的执行流调用, 当前一个流程还没有执行完, 就有其他的执行流再次进入, 我们称之为重入. 一个函数在重入的情况下, 运行结果不会出现任何不同或者任何问题, 则该函数被称为可重入函数, 否则, 是不可重入函数.

  • 调用了 malloc / free 函数, 因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数, 标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构
  • 函数是可重入的, 那就是线程安全的 .
  • 线程安全不一定是可重入的, 而可重入函数则一定是线程安全的 . 

死锁

多个执行流在对多个锁资源进程争抢操作时, 因为推进顺序不当, 而导致互相等待, 流程无法继续推进的情况.

产生死锁的四个必要条件(要产生死锁, 下面条件缺一不可)

注: 死锁这部分内容中所说的执行流可以是进程也可以是线程

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时, 对已获得的资源保持不放(一个执行流拿着A锁请求B锁, 没有请求到B锁,
                                阻塞等待时并没有释放A锁, 属于占着xx不拉x)
  • 不可剥夺条件: 一个执行流已获得的资源, 在末使用完之前, 不能强行剥夺(一个执行流加的锁, 其他的执行流不能解锁)
  • 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系避免 (一个执行流拿着A锁去请求B锁, 另一个执行
                            流拿着B锁请求A锁, 谁也拿不到谁的.)
     

预防死锁(从四个必要条件入手)

  • 加锁顺序一致 -- 破坏循环等待条件

    当多个执行流的加锁顺序一致时, 就不会产生上面所说的循环等待条件了.
     
  • 避免锁未释放的场景 -- 破坏了请求与保持条件

    如果一个执行流拿着A锁, 请求B锁失败,  则该执行流在阻塞等待前必需将所有已经拿到的锁全部解锁
     
  • 资源一次性分配 -- 破坏不可剥夺条件

    创建执行流时, 要求它申请所需的全部资源, 要么全都获取到, 要么一个也不给.

避免死锁的方法: 银行家算法(有效的避免死锁), 死锁检测算法(检测出死锁后解除死锁)

一篇关于死锁的很好的博文 : 戳链接( ̄︶ ̄)↗https://blog.csdn.net/wljliujuan/article/details/79614019


条件变量

与互斥锁不同, 条件变量是用来等待而不是用来上锁的. 条件变量用来自动阻塞一个线程, 直到某个特定事件发生(或者说某个条件满足时)为止. 通常条件变量是搭配互斥锁一起使用的 .

条件变量是利用线程间共享的全局变量进行同步的一种机制, 主要包括两个动作 :

  • 一个线程等待"条件变量的条件成立"而挂起 .
  • 另一个线程使 "条件成立" (给出条件成立信号) .

在使用时, 条件变量被用来阻塞一个线程, 当条件不满足时, 线程往往解开相应的互斥锁并等待条件发生变化. 一旦其他的某个线程改变了条件变量, 他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程. 这些线程将重新锁定互斥锁并重新测试条件是否满足.

条件变量实现同步的原理 : 

条件变量 : 一个PCB等待队列 + 向外提供的使PCB等待以及唤醒的接口

前面说到, 条件变量通常搭配互斥锁来使用, 是因为条件的检测是在互斥锁的保护下进行的, 线程在改变条件状态之前必须首先锁住互斥量, 不然就可能引发线程不安全的问题.  

如果判断条件为假, 一个执行流自动阻塞, 并释放等待状态改变的互斥锁. 如果另一个执行流改变了判断条件(使其成立), 这个执行流就会发信号给关联的条件变量, 唤醒一个或多个等待它的执行流, 重新获得互斥锁,重新判断条件.

条件变量可以通过提供的等待队列和等待唤醒接口实现线程间的同步, 但需要注意的是, "在进行条件判断时, 什么时候该等待" 其中的条件判断, 需要执行流自己来完成, 什么时候该等待, 则调用等待借口. 另一个执行流执行过程中满足了条件之后, 调用唤醒接口来唤醒等待的执行流 .

条件变量接口

条件变量类型

pthread_cond_t 类型, 是一个结构体

初始化

静态初始化 : pthread_cond_t  cont  = PTHREAD_COND_INITIALIZER ; //不需要销毁

动态初始化 : pthread_cond_init( pthread_cond_t* restrict  cond, const pthread_condattr_t*  restrict  attr)
                     //需要释放销毁(因为在堆上, 用完后需要用函数pthread_mutex_destroy来销毁)

       功能 : 初始化条件变量
       参数 : cond : 在这个条件变量上等待
                 attr : 条件的属性, 通常传入NULL,传入NULL为默认属性
                 restrict关键字 : 只用于限制指针, 告诉编译器, 所有修改该指针指向内存中内容的操作, 只能通过本指针完成. 不能
                                          通过除本指针以外的其他变量或指针修改.
       返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno 

等待

原型 : pthread_cond_wait(pthread_cond_t* restrict  cond,  pthread_mutex_t*  restrict  mutex)

功能 : 等待条件满足
           完成了三步操作: 解锁 -> 等待(加入等待的PCB队列) -> 被唤醒后重新加锁. (其中解锁和休眠是一个原子操作)
           其中, 解锁是为了让资源可以被别的执行流访问(其他执行流可能会产生可用资源), 当可用资源产生后, 再唤醒这个执
           行流, 唤醒之后需要保证操作的原子性, 又要加锁.

参数 :cond : 在这个条件变量上等待
          mutex : 给判断条件加的互斥锁

返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno 

唤醒

原型 : pthread_cond_signal(pthread_cond_t* restrict cond)

功能 : 唤醒至少一个条件变量等待队列中的执行流(可能唤醒一个, 也可能是多个)

参数 :cond : 在这个条件变量上等待

返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno 

原型 : pthread_cond_broadcast(pthread_cond_t* restrict cond)

功能 : 广播唤醒所有条件变量等待队列中的执行流

参数 :cond : 在这个条件变量上等待

返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno 

销毁

原型 : pthread_cond_destroy(pthread_cond_t* cond)

功能 : 在条件变量不再使用之后, 销毁释放资源(只针对init函数初始化的条件变量)

参数 : 要销毁的条件变量的地址

返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno

来看几个例子理解一下条件变量, 厨师做好吃的, 吃货来吃的例子

1 对 1的模式, 一位厨师, 一位吃货, 为了杜绝浪费, 厨师一次只做一份美食. 等待吃货来吃, 当吃货吃完之后, 等待厨师再做美食.

#include<iostream>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<pthread.h>
using namespace std;

int cate = 0;
pthread_mutex_t mutex;
pthread_cond_t cond;
void* foodie(void* arg){//吃货线程
    while(1){
        pthread_mutex_lock(&mutex);
        while(cate == 0){//没有美食只能等等着厨师做好之后叫我了
            pthread_cond_wait(&cond, &mutex);
        }
        --cate;
        cout << "吃货:有好吃的, 马上吃一碗\n";
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
    }
    return NULL;
}
void* chief(void* arg){//厨师线程
    while(1){
        pthread_mutex_lock(&mutex);
        while(cate == 1){
            pthread_cond_wait(&cond, &mutex);
        }
        ++cate;//做一份美食, 唤醒一下吃货, 别睡了, 起来吃好吃的啦
        cout << "大厨:本大厨做了一份美食\n";
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
    }
}
int main(){
    pthread_t foodie_tid, chief_tid;
    int ret;
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);
    ret = pthread_create(&foodie_tid, NULL, foodie, NULL);
    if(ret){
        fprintf(stderr, "foodie_thread create:%s\n", strerror(ret));
            return -1;
    }
    ret = pthread_create(&chief_tid, NULL, chief, NULL);
    if(ret){
        fprintf(stderr, "chief_thread create:%s\n", strerror(ret));
        return -1;
    }
    pthread_join(foodie_tid, NULL);
    pthread_join(chief_tid, NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

    

n : n , 当有多个厨师和多个吃货时 .

#include<iostream>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<pthread.h>
using namespace std;

int cate = 0;
pthread_mutex_t mutex;
pthread_cond_t cond;
void* foodie(void* arg){//吃货线程
    while(1){
        pthread_mutex_lock(&mutex);
        while(cate == 0){//没有美食只能等等着厨师做好之后叫我了
            pthread_cond_wait(&cond, &mutex);
        }
        --cate;
        cout << "吃货:有好吃的, 马上吃一碗\n";
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
    }
    return NULL;
}
void* chief(void* arg){//厨师线程
    while(1){
        pthread_mutex_lock(&mutex);
        while(cate == 1){
            pthread_cond_wait(&cond, &mutex);
        }
        ++cate;//做一份美食, 唤醒一下吃货, 别睡了, 起来吃好吃的啦
        cout << "大厨:本大厨做了一份美食\n";
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
    }
}
int main(){
    pthread_t foodie_tid[5], chief_tid[5];
    int ret;
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);
    for(int i = 0; i < 5; ++i){
        ret = pthread_create(&foodie_tid[i], NULL, foodie, NULL);
        if(ret){
            fprintf(stderr, "foodie_thread%d create:%s\n", i + 1, strerror(ret));
            return -1;
        }
    
    }
    for(int i = 0; i < 5; ++i){
        ret = pthread_create(&chief_tid[i], NULL, chief, NULL);
        if(ret){
            fprintf(stderr, "chief_thread%d create:%s\n", i + 1, strerror(ret));
            return -1;
        }
    }
    for(int i = 0; i < 5; pthread_join(foodie_tid[i], NULL), ++i);
    for(int i = 0; i < 5; pthread_join(chief_tid[i], NULL), ++i);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

当变成多个厨师与多个吃货的时候, 运行发现, 运行一下就卡死了, 这是为什么呢?

条件变量使用规范

1. 不同的执行流应该等待在不同的条件变量上

上面的代码运行卡死的原因 :

原因是当厨师线程和吃货线程共用一个条件变量时, 也就意味着共用了同一个PCB的等待队列,  例如这个场景, 一个吃货线程吃完美食之后, 本想唤醒厨师线程, 但条件变量的等待队列中的PCB是吃货线程和厨师线程的都有, 所以就有可能唤醒的是吃货线程, 而此时已经没有美食了, 这个吃货线程就会被阻塞陷入等待, 但这时候厨师线程都在等待队列中被阻塞着, 又没人再唤醒厨师, 所以厨师线程和吃货线程双双陷入等待, 所以就执行不下去了. 所以说, 我们应该让不同的执行流等待在不同的条件变量上, 该唤醒哪个就唤醒哪个, 避免逻辑混乱.

2. 条判断条件变量的条件时应使用循环判断(如while)

为什么呢, 这和条件变量的唤醒函数有关, pthread_cond_signal()函数唤醒条件变量的等待队列中的线程时, 唤醒至少一个, 也就是说, 可能会唤醒多个, 甚至全部. 还有pthread_cond_broadcast()是唤醒全部.  例如当唤醒(在条件变量等待队列中的)多个吃货线程后, 其中只会有一个线程加锁成功, 往下执行吃完美食后再解锁(其他的吃货线程在阻塞等待锁资源),  这时(还没来得及唤醒厨师线程)在等待锁资源的其他吃货线程可能就会抢到锁(此时已经没有美食了),  如果不在判断一遍条件变量的条件的话, 该线程就会往下执行去吃美食, 这样就会造成逻辑混乱, 所以, 条件变量的条件一定要循环判断, 避免逻辑混乱.

来看下面改进好的代码 

#include<iostream>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<pthread.h>
using namespace std;

int cate = 0;
pthread_mutex_t mutex;
pthread_cond_t foodie_cond;
pthread_cond_t chief_cond;
void* foodie(void* arg){//吃货线程
    int n = *(int*)arg;
    while(1){
        pthread_mutex_lock(&mutex);
        while(cate == 0){//没有美食只能等等着厨师做好之后叫我了
            pthread_cond_wait(&foodie_cond, &mutex);
        }
        --cate;
        cout << "吃货" << n <<":有好吃的, 马上吃一碗\n";
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&chief_cond);
    }
    return NULL;
}
void* chief(void* arg){//厨师线程
    int n = *(int*)arg;
    while(1){
        pthread_mutex_lock(&mutex);
        while(cate == 1){
            pthread_cond_wait(&chief_cond, &mutex);
        }
        ++cate;//做一份美食, 唤醒一下吃货, 别睡了, 起来吃好吃的啦
        cout << "大厨" << n <<":本大厨做了一份美食\n";
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&foodie_cond);
    }
}
int main(){
    pthread_t foodie_tid[5], chief_tid[5];
    int n[5] = {1, 2, 3, 4, 5};
    int ret;
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&foodie_cond, NULL);
    pthread_cond_init(&chief_cond, NULL);
    for(int i = 0; i < 5; ++i){
        ret = pthread_create(&foodie_tid[i], NULL, foodie, (void*)(n + i));
        if(ret){
            fprintf(stderr, "foodie_thread%d create:%s\n", i + 1, strerror(ret));
            return -1;
        }
    
    }
    for(int i = 0; i < 5; ++i){
        ret = pthread_create(&chief_tid[i], NULL, chief, (void*)(n + i));
        if(ret){
            fprintf(stderr, "chief_thread%d create:%s\n", i + 1, strerror(ret));
            return -1;
        }
    }
    for(int i = 0; i < 5; pthread_join(foodie_tid[i], NULL), ++i);
    for(int i = 0; i < 5; pthread_join(chief_tid[i], NULL), ++i);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&foodie_cond);
    pthread_cond_destroy(&chief_cond);
    return 0;
}

为什么pthread_cond_wait()中要传入互斥锁?

某个执行流在判断条件变量的条件是否满足之前, 要先给条件变量加锁, 如果不满足条件, 则这个执行流阻塞进入等待队列, 但此时这个执行流并没有释放条件变量的锁, 在这个执行流等待过程中, 没有其他的执行流能访问修改条件变量的判断条件(因为加的锁没解锁). 这就导致了其他执行流因为获取不到锁资源而陷入等待(没人修改条件判断), 而这个在条件变量的等待队列执行中等待的执行流因为没有其他执行流唤醒而一直在等待, 这就造成了死锁.

所以说, 在进入条件变量的等待队列之前, 需要先给自己所拥有的锁解锁, 再进入等待, 这样其他的执行流才能拿到条件变量的判断条件的锁资源, 然后修改判断条件, 从而唤醒这个等待的执行流 .

为什么互斥锁和条件变量要配合使用?

互斥锁体现的是一种竞争, 我离开了, 通知你进来.   

条件变量体现的是一种协作, 我准备好了, 通知你开始吧.

简单来说, 是为了效率, 即为了避免有线程不断轮循检查条件是否满足而降低效率, 具体来看 :

两个线程操作同一临界区时, 通过互斥锁保护, 若A线程已经加锁, B线程再加锁时候会被阻塞, 直到A释放锁, B再获得锁运行, 进程B必须不停的主动获得锁, 检查条件, 释放锁, 再获得锁, 再检查, 再释放.  一直到满足运行的条件的时候才可以 (而此过程中其他线程一直在等待该线程的结束), 这种方式是比较消耗系统的资源的. 而条件变量同样是阻塞, 但需要通知才能唤醒, 线程被唤醒后, 它将重新检查判断条件是否满足, 如果还不满足, 该线程就又阻塞在这里, 等待条件满足后被唤醒, 节省了线程不断检查条件浪费的资源. 这个过程一般用while语句实现.  当线程B发现被锁定的变量不满足条件时会自动的释放锁并把自身置于等待状态, 让出CPU的控制权给其它线程. 其它线程此时就有机会去进行操作, 当修改完成后再通知那些由于条件不满足而陷入等待状态的线程. 这是一种通知模型的同步方式, 大大的节省了CPU的计算资源, 减少了线程之间的竞争, 而且提高了线程之间的系统工作的效率. 

发布了223 篇原创文章 · 获赞 639 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_41071068/article/details/104641225