Linux 线程互斥

[复制链接]
发表于 2025-9-23 01:00:18 | 显示全部楼层 |阅读模式
目次

Linux线程互斥
历程线程间的互斥相关配景概念
互斥量的接口
初始化互斥量
互斥量加锁息争锁
锁的封装
​编辑
互斥量加锁的非壅闭版本
互斥量实现原理探究
可重入VS线程安全
概念
常见的线程不安全的环境
常见的线程安全的环境
常见的不可重入的环境
常见的可重入的环境
可重入与线程安全接洽
可重入与线程安全区别
常见锁概念
死锁
死锁的四个须要条件
避免死锁
避免死锁算法
Linux线程同步
条件变量
条件变量函数
条件变量利用规范


Linux线程互斥

历程线程间的互斥相关配景概念



  • 临界资源: 多线程实验流共享的资源叫做临界资源。
  • 临界区: 每个线程内部,访问临界资源的代码,就叫做临界区。
  • 互斥: 任何时间,互斥包管有且只有一个实验流进入临界区,访问临界资源,通常对临界资源起保护作用。
  • 原子性: 不会被任何调度机制打断的利用,该利用只有两态,要么完成,要么未完成。
   对一个全局变量举行多线程并发 -- / ++ 利用是否是安全的?
  我们来试一下模拟多线程举行抢票的场景看会出现什么题目:
  1. #include <iostream>
  2. #include <vector>
  3. #include <cstdio>
  4. #include <string>
  5. #include <unistd.h>
  6. #define NUM 5
  7. using namespace std;
  8. int tickets = 1000;
  9. void *ThreadRoutine(void *args)
  10. {
  11.     int i = (uint64_t)args;
  12.     while(true)
  13.     {     
  14.         if(tickets > 0)
  15.         {
  16.             usleep(1000);
  17.             printf("who=thread->%d get a ticket: %d\n", i, tickets);
  18.             --tickets;
  19.         }
  20.         else
  21.         {
  22.             break;
  23.         }
  24.     }
  25.     pthread_exit((void*)666);
  26. }
  27. int main()
  28. {
  29.     pthread_t tid;
  30.     vector<pthread_t> tids;
  31.     for(int i = 0; i < NUM; i++)
  32.     {
  33.         pthread_create(&tid, nullptr, ThreadRoutine, (void*)i);
  34.         tids.push_back(tid);
  35.     }
  36.     for(int i = 0; i < NUM; i++)
  37.     {
  38.         pthread_join(tids[i], nullptr);
  39.     }
  40.     return 0;
  41. }
复制代码
运行效果显然不符合我们的预期,由于此中出现了剩余票数为负数的环境。

   为什么无法获得预期效果?
  

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,大概有许多个线程会进入该代码段
  • --ticket 利用本身就不是一个原子利用
   为什么--ticket利用不是一个原子利用?
  -- 利用并不是原子利用,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中
  • update : 更新寄存器内里的值,实验-1利用
  • store :将新值,从寄存器写回共享变量ticket的内存地点

我们取出--tickets的汇编代码
  1. movl    tickets(%rip), %eax                          
  2. subl    $1, %eax                                    
  3. movl    %eax, tickets(%rip)
复制代码
既然--利用必要三个步调才气完成,那么就有大概当thread1刚把tickets的值读进CPU就被切走了,也就是从CPU上剥离下来,假设此时thread1读取到的值就是1000,而当thread1被切走时,寄存器中的1000叫做thread1的上下文信息,因此必要被保存起来,之后thread1就被挂起了。

假设此时thread2被调度了,由于thread1只举行了--利用的第一步,因此thread2此时看到tickets的值照旧1000,而体系给thread2的时间片大概较多,导致thread2一次性实验了100次--才被切走,终极tickets由1000减到了900。

此时体系再把thread1规复上来,规复的本质就是继承实验thread1的代码,而且要将thread1曾经的硬件上下文信息规复出来,此时寄存器当中的值是规复出来的1000,然后thread1继承实验--利用的第二步和第三步,终极将999写回内存。

在上述过程中,thread1抢了1张票,thread2抢了100张票,而此时剩余的票数却是999,也就相当于多出了100张票。因此对一个变量举行--利用并不是原子的,虽然--tickets看起来就是一行代码,但这行代码被编译器编译后本质上是三行汇编,相反,对一个变量举行++也必要对应的三个步调,即++利用也不是原子利用。
tips:
寄存器不即是寄存器上下文,差别线程寄存器内容不一样
   回到卖票场景: 这就是共享资源在被线程在并发访问时而导致的数据不一致题目
  

要办理上述抢票体系的题目,必要做到三点:


  • 代码必须有互斥活动:当代码进入临界区实验时,不答应其他线程进入该临界区。
  • 如果多个线程同时要求实验临界区的代码,而且此时临界区没有线程在实验,那么只能答应一个线程进入该临界区。
  • 如果线程不在临界区中实验,那么该线程不能制止其他线程进入临界区。
要做到这三点,本质上就是必要一把锁。Linux上提供的这把锁叫互斥量。

互斥量的接口

初始化互斥量

初始化互斥量有两种方法:
方法一:动态分配
利用pthread_mutex_init对互斥量举行初始化
函数原型如下:
  1. int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
复制代码
参数阐明:


  • mutex:必要初始化的互斥量。
  • attr:初始化互斥量的属性,一样平常设置为NULL即可。
返回值阐明:


  • 互斥量初始化乐成返回0,失败返回错误码。
方法二:静态分配
  1. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
复制代码
  烧毁互斥量
  烧毁互斥量的函数叫做pthread_mutex_destroy,该函数的函数原型如下:
  1. int pthread_mutex_destroy(pthread_mutex_t *mutex);
复制代码
参数阐明:


  • mutex:必要烧毁的互斥量。
返回值阐明:


  • 互斥量烧毁乐成返回0,失败返回错误码。
烧毁互斥量必要注意:


  • 利用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不必要烧毁。
  • 不要烧毁一个已经加锁的互斥量。
  • 已经烧毁的互斥量,要确保背面不会有线程再实验加锁。
互斥量加锁息争锁

  1. int pthread_mutex_lock(pthread_mutex_t *mutex);
  2. int pthread_mutex_unlock(pthread_mutex_t *mutex);
  3. 返回值:成功返回0,失败返回错误码
复制代码
调用 pthread_mutex_lock 时,大概会遇到以下环境:


  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回乐成
  • 发起函数调用时,其他线程已经锁定互斥量,大概存在其他线程同时申请互斥量,但没有竞争到互斥量, 那么pthread_mutex_lock调用会陷入壅闭(实验流被挂起),等候互斥量解锁。
   改进上面的售票体系
  比方,我们在上述的抢票体系中引入互斥量,每一个线程要进入临界区之前都必须先申请锁,只有申请到锁的线程才可以进入临界区对临界资源举行访问,而且当线程出临界区的时间必要开锁,如许才气让别的要进入临界区的线程继承竞争锁。
  1. #include <iostream>#include <vector>#include <cstdio>#include <string>#include <unistd.h>#define NUM 5using namespace std;int tickets = 1000;pthread_mutex_t mutex;//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  2.   如许写就不消对互斥量举行初始化和烧毁了void *ThreadRoutine(void *args){    int i = (uint64_t)args;    while(true)    {             pthread_mutex_lock(&mutex);        if(tickets > 0)        {            usleep(1000);            printf("who=thread->%d get a ticket: %d\n", i, tickets);            --tickets;          }        else        {            break;        }        pthread_mutex_unlock(&mutex);    }    pthread_exit((void*)666);}int main(){    pthread_mutex_init(&mutex);    pthread_t tid;    vector<pthread_t> tids;    for(int i = 0; i < NUM; i++)    {        pthread_create(&tid, nullptr, ThreadRoutine, (void*)i);        tids.push_back(tid);    }    for(int i = 0; i < NUM; i++)    {        pthread_join(tids[i], nullptr);    }    pthread_mutex_destroy(&mutex);    return 0;}
复制代码
运行代码,此时在抢票过程中就不会出现票数剩余为负数的环境了。

但是我们可以注意到抢票的过程全都是由线程3完成的
   题目1:票被一个线程抢完
  办理方法:让该线程进入短暂休眠

运行效果:

虽然办理了锁竞争题目,但是它并没有按我们的代码逻辑运行,当票数剩余1时全部线程应该会竣事运行
   题目2:线程实验完使命之后为什么照旧处于壅闭状态
  

办理方法:


注意:


  • 在大部门环境下,加锁本身都是有损于性能的事,但是为了包管在多实验流下的线程安全,我们不得倒霉用锁,以是加锁的本质就是利用时间调换安全,加锁的表现就是线程对于临界区的资源串行访问。
  • 我们应该在符合的位置举行加锁息争锁,如许能尽大概淘汰加锁带来的性能开销成本,临界区代码越少越好。
  • 举行临界资源的保护,是全部实验流都应该服从的尺度,这时步伐员在编码时必要注意的。
锁的封装(RAII风格的锁)

  1. #ifndef __LOCKGUARD_HPP__
  2. #define __LOCKGUARD_HPP__
  3. #include <iostream>
  4. #include <pthread.h>
  5. using namespace std;
  6. class Mutex
  7. {
  8. public:
  9.     Mutex(pthread_mutex_t *lock)
  10.         :_lock(lock)
  11.     {}
  12.     void lock()
  13.     {
  14.         pthread_mutex_lock(_lock);
  15.     }
  16.     void unlock()
  17.     {
  18.         pthread_mutex_unlock(_lock);
  19.     }
  20.     ~Mutex()
  21.     {}
  22. private:   
  23.     pthread_mutex_t *_lock;
  24. };
  25. class LockGuard
  26. {
  27. public:
  28.     LockGuard(pthread_mutex_t *lock)
  29.         :_mutex(lock)
  30.     {
  31.         _mutex.lock();
  32.     }
  33.     ~LockGuard()
  34.     {
  35.         _mutex.unlock();
  36.     }
  37. private:
  38.     Mutex _mutex;
  39. };
  40. #endif
复制代码
  1. #include <iostream>
  2. #include <vector>
  3. #include <cstdio>
  4. #include <string>
  5. #include <unistd.h>
  6. #include "LockGuard.hpp"
  7. #define NUM 5
  8. using namespace std;
  9. int tickets = 1000;
  10. pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
  11. void *ThreadRoutine(void *args)
  12. {
  13.     int i = (uint64_t)args;
  14.     while(true)
  15.     {     
  16.         {
  17.             LockGuard mutex(&lock);
  18.             if(tickets > 0)
  19.             {
  20.                 usleep(1000);
  21.                 printf("who=thread->%d get a ticket: %d\n", i, tickets);
  22.                 --tickets;  
  23.             }
  24.             else
  25.             {
  26.                 break;
  27.             }
  28.         }
  29.     }
  30.     pthread_exit((void*)666);
  31. }
  32. int main()
  33. {
  34.     pthread_t tid;
  35.     vector<pthread_t> tids;
  36.     for(int i = 0; i < NUM; i++)
  37.     {
  38.         pthread_create(&tid, nullptr, ThreadRoutine, (void*)i);
  39.         tids.push_back(tid);
  40.     }
  41.     for(int i = 0; i < NUM; i++)
  42.     {
  43.         pthread_join(tids[i], nullptr);
  44.     }
  45.     return 0;
  46. }
复制代码
运行效果:


互斥量加锁的非壅闭版本

  1. int pthread_mutex_trylock(pthread_mutex_t *mutex);
复制代码
功能如果申请锁乐成,则返回0,否则返回不为0的错误码,与pthread_mutex_lock相比,他在申请锁失败时,不会举行壅闭等候
互斥量实现原理探究

   加锁后的原子性体如今那里?
  每一个线程在进入临界区访问临界区资源时都必要先申请同一份锁资源,在其他线程看来申请到锁的线程只有两种状态,要么持有锁,要么开释锁,由于只有这两种状态对其他线程才是故意义的。
对于thread-1 thread-2 thread-3来说,它们只关心thread-1是否持有锁大概开释锁,由于只有thread-1的这两种状态才对其他线程故意义

此时,对于thread-1 thread-2 thread-3来说thread-1的利用过程就是原子性的
   临界区内的线程大概举行线程切换吗?
  会的,在线程任何地方都会被切换

但是线程是被持有锁切出去的,以是在该线程不在期间,其他线程是没有权限访问临界区的
   锁是否必要被保护?
  我们说被多个实验流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。全部的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个实验流共享的资源,也就是说锁本身就是临界资源。
既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?
锁现实上是本身保护本身的,申请锁和开释锁本身就是原子性的,那么锁天然就是安全的。
   怎样包管申请锁的过程是原子的?
  

  • 上面我们已经阐明了--和++利用不是原子利用,大概会导致数据不一致题目。
  • 为了实现互斥锁利用,大多数体系结构都提供了swap或exchange指令,该指令的作用就是把寄存器和内存单位的数据相互换。
  • 由于只有一条指令,包管了原子性,纵然是多处理器平台,访问内存的总线周期也有先后,一个处理器上的互换指令实验时,另一个处理器的互换指令只能等候总线周期。
上面我们已经阐明了--和++利用不是原子利用,大概会导致数据不一致题目。
为了实现互斥锁利用,大多数体系结构都提供了swap或exchange指令,该指令的作用就是把寄存器和内存单位的数据相互换。
由于只有一条指令,包管了原子性,纵然是多处理器平台,访问内存的总线周期也有先后,一个处理器上的互换指令实验时,另一个处理器的互换指令只能等候总线周期。
   利用体系的工作原理:
• 利用系同一旦启动乐成后就是一个死循环。
• 时钟是盘算机中的一个硬件,时钟每隔一段时间会向利用体系发起一个时钟停止,利用体系就会根据时钟停止去实验停止向量表。
• 停止向量表本质上就是一个函数表,比如刷磁盘的函数、检测网卡的函数以及革新数据的函数等等。
• 盘算机不停向利用体系发起时钟停止,利用体系就根据时钟停止,不停地去实验对应的代码。
• CPU有多个,但总线只有一套。CPU和内存都是盘算机中的硬件,这两个硬件之间要举行数据交互一定是用线毗连起来的,此中我们把CPU和内存毗连的线叫做体系总线,把内存和外设毗连起来的线叫做IO总线。
• 体系总线只有一套,有的时间CPU访问内存是想从内存中读取指令,有的时间是想从内存读取数据,以是总线是被差别的利用种类共享的。盘算机是通过总线周期来区分此时总线当中传输的是哪种资源的。
  下面我们来看看lock和unlock的伪代码:
  1. lock:
  2.     movb $0, % al
  3.     xchgb %al, mutex
  4.     if (al 寄存器的内容> 0){
  5.         return 0;
  6.     } else
  7.         挂起等待;
  8.     goto lock;
  9. unlock:
  10.     movb $1, mutex
  11.     唤醒等待 Mutex 的线程;
  12.     return 0;
复制代码
我们可以以为mutex的初始值为1,al是盘算机中的一个寄存器,当线程申请锁时,必要实验以下步调:


  • 先将al寄存器中的值清0。
  • 然后互换al寄存器和mutex中的值。xchgb是体系结构提供的互换指令,该指令可以完成寄存器和内存单位之间数据的互换。
  • 末了判断al寄存器中的值是否大于0。若大于0则申请锁乐成,否则,申请锁失败必要被挂起等候
比方,此时内存中mutex的值为1,线程申请锁时先将al寄存器中的值清0,然后将al寄存器中的值与内存中mutex的值举行互换。

互换完成后检测该线程的al寄存器中的值为1,则该线程申请锁乐成,可以进入临界区对临界资源举行访问。
而今后的线程如果再申请锁,与内存中的mutex互换得到的值就是0了,此时该线程申请锁失败,必要被挂起等候,直到锁被开释后再次竞争申请锁。

当线程开释锁时,必要实验以下步调:

  • 将内存中的mutex置回1。使得下一个申请锁的线程在实验互换指令后能够得到1,形象地说就是“将锁的钥匙放归去”。
  • 唤醒等候Mutex的线程。唤醒这些由于申请锁失败而被挂起的线程,让它们继承竞争申请锁。
任何时间都线程都有大概被切换,但是只有一个线程能得到锁资源,以是全部线程都有大概得到锁资源,就看那个线程先申请到

注意:


  • 在申请锁时本质上就是哪一个线程先实验了互换指令,那么该线程就申请锁乐成,由于此时该线程的al寄存器中的值就是1了。而互换指令就只是一条汇编指令,一个线程要么实验了互换指令,要么没有实验互换指令,以是申请锁的过程是原子的。
  • 在线程开释锁时没有将当前线程al寄存器中的值清0,这不会造成影响,由于每次线程在申请锁时都会先将本身al寄存器中的值清0,再实验互换指令。
  • CPU内的寄存器不是被全部的线程共享的,每个线程都有本身的一组寄存器,但内存中的数据是各个线程共享的。申请锁现实就是,把内存中的mutex通过互换指令,原子性的互换到本身的al寄存器中。
着实申请锁和开释锁在底层本质就一句汇编:以是加锁和开释锁都是原子的
  1. lock:
  2.     xchgb %al, mutex //将线程al寄存器与mutex的资源进行交换
  3.   
  4. unlock:
  5.     movb $1, mutex //将mutex的资源置1
复制代码
可重入VS线程安全

概念

线程安全: 多个线程并发同一段代码时,不会出现差别的效果。常见对全局变量大概静态变量举行利用,而且没有锁保护的环境下,会出现线程安全题目。
重入: 同一个函数被差别的实验流调用,当前一个流程还没有实验完,就有其他的实验流再次进入,我们称之为重入。一个函数在重入的环境下,运行效果不会出现任何差别大概任何题目,则该函数被称为可重入函数,否则是不可重入函数。
注意: 线程安全讨论的是线程实验代码时是否安全,重入讨论的是函数被重入进入。
常见的线程不安全的环境

1. 不保护共享变量的函数。
2. 函数状态随着被调用,状态发生变化的函数。
3. 返回指向静态变量指针的函数。
4. 调用线程不安全函数的函数。
常见的线程安全的环境

每个线程对全局变量大概静态变量只有读取的权限,而没有写入的权限,一样平常来说这些线程是安全的。
类大概接口对于线程来说都是原子利用。
多个线程之间的切换不会导致该接口的实验效果存在二义性。
常见的不可重入的环境

调用了malloc/free函数,由于malloc函数是用全局链表来管理堆的。
调用了尺度I/O库函数,尺度I/O可以的许多实现都是以不可重入的方式利用全局数据结构。
可重入函数体内利用了静态的数据结构。
常见的可重入的环境

倒霉用全局变量或静态变量。
倒霉用malloc大概new开辟出的空间。
不调用不可重入函数。
不返回静态或全局数据,全部数据都由函数的调用者提供。
利用当地数据,大概通过制作全局数据的当地拷贝来保护全局数据。
可重入与线程安全接洽

函数是可重入的,那就是线程安全的。
函数是不可重入的,那就不能由多个线程利用,有大概引发线程安全题目。
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别

可重入函数是线程安全函数的一种。
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数的锁还未开释则会产生死锁,因此是不可重入的。
   明白四个概念即可
  线程安全:多实验流实验时产生的并发题目
可重入:函数特点题目
不可重入函数:在被多个实验流重入的环境下大概会遇到线程安全题目
可重入函数:在被多个实验流重入的环境下不会遇到线程安全题目
常见锁概念

死锁

死锁是指在一组历程中的各个历程均占据不会开释的资源,但因互相申请被其他历程所站用不会开释的资源而处于的一种永世等候状态。
   单实验流大概产生死锁吗?
  如果某一实验流连续申请了两次锁,那么此时该实验流就会被挂起。由于该实验流第一次申请锁乐成,但第二次申请锁时由于该锁已经被申请过了,于是申请失败导致被挂起直到该锁被开释时才会被唤醒,但是这个锁原来就在本身手上,本身如今处于被挂起的状态根本没有时机开释锁,以是该实验流将永世不会被唤醒,此时该实验流也就处于一种死锁的状态。
比方,在下面的代码中我们让主线程创建的新线程连续申请了两次锁
  1. #include <iostream>
  2. #include <cstdio>
  3. #include <pthread.h>
  4. using namespace std;
  5. pthread_mutex_t lock;
  6. void *threadRoutine(void *args)
  7. {
  8.     pthread_mutex_lock(&lock);
  9.     pthread_mutex_lock(&lock);
  10.     pthread_exit((void*)6666);
  11. }
  12. int main()
  13. {
  14.     pthread_mutex_init(&lock, nullptr);
  15.     pthread_t tid;
  16.     pthread_create(&tid, nullptr, threadRoutine, nullptr);   
  17.     pthread_join(tid, nullptr);
  18.     pthread_mutex_destroy(&lock);
  19.     return 0;
  20. }
复制代码
用ps下令检察该历程时可以看到,该历程当前的状态是Sl+,此中的l现实上就是lock的意思,表现该历程当前处于一种死锁的状态,而右边的步伐处于挂起状态。

   什么叫做壅闭?
  历程运行时是被CPU调度的,换句话说历程在调度时是必要用到CPU资源的,每个CPU都有一个运行等候队列(runqueue),CPU在运行时就是从该队列中获取历程举行调度的,同时陪同一个等候队列,处于等候队列或没被cpu实验的历程,这种状态我们就做壅闭状态。

在运行等候队列中的历程本质上就是在等候CPU资源,现实上不止是等候CPU资源云云,等候其他资源也是云云,比如锁的资源、磁盘的资源、网卡的资源等等,它们都有各自对应的资源等候队列。

   比方,当某一个历程在被CPU调度时,该历程必要用到锁的资源,但是此时锁的资源正在被其他历程利用:
  那么此时该历程的状态就会由R状态变为某种壅闭状态,比如S状态。而且该历程会被移出运行等候队列,被链接到等候锁的资源的资源等候队列,而CPU则继承调度运行等候队列中的下一个历程。
今后若尚有历程必要用到这一个锁的资源,那么这些历程也都会被移出运行等候队列,依次链接到这个锁的资源等候队列当中。
直到利用锁的历程已经利用完毕,也就是锁的资源已经就绪,此时就会从锁的资源等候队列中唤醒一个历程,将该历程的状态由S状态改为R状态,并将其重新链接到运行等候队列,比及CPU再次调度该历程时,该历程就可以利用到锁的资源了。
  总结一下:


  • 站在利用体系的角度,历程等候某种资源,就是将当前历程的task_struct放入对应的资源等候队列,这种环境可以称之为当前历程被挂起等候了。
  • 站在用户角度,当历程等候某种资源时,用户看到的就是本身的历程卡住不动了,我们一样平常称之为应用壅闭了。
这里所说的资源可以是硬件资源也可以是软件资源,锁本质就是一种软件资源,当我们申请锁时,锁当前大概并没有就绪,大概正在被其他线程所占用,此时当其他线程再来申请锁时,就会被放到这个锁的资源等候队列当中。
死锁的四个须要条件



  • 互斥条件(条件): 一个资源每次只能被一个实验流利用。
  • 哀求与保持条件(原则): 一个实验流因哀求资源而壅闭时,对已获得的资源保持不放。
  • 不剥夺条件(原则): 一个实验流已获得的资源,在未利用完之前,不能强行剥夺。
  • 循环等候条件: 多少实验流之间形成一种头尾相接的循环等候资源的关系。

注意: 这是死锁的四个须要条件,只要产生死锁就一定满意了这四个条件。
避免死锁



  • 粉碎死锁的四个须要条件。
1. 倒霉用锁  2. 开释掉此中一个人的锁 3. 按序次申请锁资源


  • 加锁序次一致。
  • 避免锁未开释的场景。
  • 资源一次性分配。
避免死锁算法

死锁检测算法 
银行家算法
Linux线程同步

   同步: 在包管数据安全的条件下,让线程能够按照某种特定的序次访问临界资源,从而有效避免饥饿题目,这就叫做同步。
竞态条件: 由于时序题目,而导致步伐异常,我们称之为竞态条件。
  起首必要明确的是,单纯的加锁是会存在某些题目的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后什么也不做,以是在我们看来这个线程就不停在申请锁和开释锁,这就大概导致其他线程长时间竞争不到锁,引起饥饿题目
单纯的加锁是没有错的,它能够包管在同一时间只有一个线程进入临界区,但它没有高效的让每一个线程利用这份临界资源。
如今我们增长一个规则,当一个线程开释锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等候队列的末了大概退出。
增长这个规则之后,下一个获取到锁的资源的线程就一定是在资源等候队列首部的线程,如果有十个线程,此时我们就能够让这十个线程按照某种序次举行临界资源的访问。
   比方,如今有两个线程访问一块临界区,一个线程往临界区写入数据,另一个线程从临界区读取数据,但负责数据写入的线程的竞争力特别强,该线程每次都能竞争到锁,那么此时该线程就不停在实验写入利用,直到临界区被写满,今后该线程就不停在举行申请锁和开释锁。而负责数据读取的线程由于竞争力太弱,每次都申请不到锁,因此无法举行数据的读取,引入同步后该题目就能很好的办理。
  条件变量

   条件变量是利用线程间共享的全局变量举行同步的一种机制,条件变量是用来形貌某种资源是否就绪的一种数据化形貌。
  条件变量重要包罗两个动作:


  • 一个线程等候条件变量的条件成立而被挂起。(申请锁失败的线程,被放入资源等候队列,等候锁资源就绪)
  • 另一个线程使条件成立后唤醒等候的线程。(持有锁的线程,开释锁资源,关照条件变量唤醒等候的线程)
条件变量通常必要共同互斥锁一起利用。
   在纯互斥条件下,由于一个锁竞争本领太强,会导致其他线程的饥饿题目(其他线程不停处于壅闭状态,不能及时完成它们的工作)
  

办理方法:利用条件变量举行同步

条件变量函数

   初始化条件变量
  初始化条件变量的函数叫做pthread_cond_init,该函数的函数原型如下:
  1. int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
复制代码
参数阐明:


  • cond:必要初始化的条件变量。
  • attr:初始化条件变量的属性,一样平常设置为NULL即可。
返回值阐明:


  • 条件变量初始化乐成返回0,失败返回错误码。
调用pthread_cond_init函数初始化条件变量叫做动态分配,除此之外,我们还可以用下面这种方式初始化条件变量,该方式叫做静态分配:
  1. pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
复制代码
  烧毁条件变量
  烧毁条件变量的函数叫做pthread_cond_destroy,该函数的函数原型如下:
  1. int pthread_cond_destroy(pthread_cond_t *cond);
复制代码
参数阐明:


  • cond:必要烧毁的条件变量。
返回值阐明:


  • 条件变量烧毁乐成返回0,失败返回错误码。
烧毁条件变量必要注意:


  • 利用PTHREAD_COND_INITIALIZER初始化的条件变量不必要烧毁。
   等候条件变量满意
  等候条件变量满意的函数叫做pthread_cond_wait,该函数的函数原型如下:
  1. int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
复制代码
参数阐明:


  • cond:必要等候的条件变量。
  • mutex:当前线程所处临界区对应的互斥锁
返回值阐明:


  • 函数调用乐成返回0,失败返回错误码。
   唤醒等候
  唤醒等候的函数有以下两个:
  1. int pthread_cond_broadcast(pthread_cond_t *cond);
  2. int pthread_cond_signal(pthread_cond_t *cond);
复制代码
区别:


  • pthread_cond_signal函数用于唤醒等候队列中首个线程。
  • pthread_cond_broadcast函数用于唤醒等候队列中的全部线程。
参数阐明:


  • cond:唤醒在cond条件变量下等候的线程。
返回值阐明:


  • 函数调用乐成返回0,失败返回错误码。
   利用示例:
  比方,下面我们用主线程创建五个新线程,让主线程控制这五个新线程活动。这五个新线程创建后都在条件变量下举行等候,直到主线程唤醒一个等候线程,云云举行下去。
  1. #include <iostream>#include <cstdio>#include <pthread.h>#include <unistd.h>using namespace std;#define NUM 5int cnt = 0;pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  2. void *threadRoutine(void *args){    pthread_detach(pthread_self());    uint64_t i = (uint64_t)args;    while(true)    {        sleep(1);        pthread_mutex_lock(&lock);        pthread_cond_wait(&cond, &lock); //申请锁的同时会开释锁        cout << "thread-> " << i << " cnt: " << cnt++ << endl; //表现器文件, cnt都属于临界资源        pthread_mutex_unlock(&lock);        usleep(1000);    }    pthread_exit((void*)666);}int main(){    pthread_t tid;    for(int i = 0; i < NUM; i++)    {        pthread_create(&tid, nullptr, threadRoutine, (void*)i);    }    //主线程开释其他线程就会主动开释    while(true)    {        pthread_cond_signal(&cond); //唤醒该队列下首个线程        //主线程唤醒在锁资源等候队列的线程        sleep(1);    }    return 0;}
复制代码

如果我们想每次唤醒都将在该条件变量下等候的全部线程举行唤醒,可以将代码中的pthread_cond_signal函数改为pthread_cond_broadcast函数。

tips: C++源文件的后缀可以是.cpp、.cc、.cxx。
   为什么pthread_cond_wait必要互斥量
  

  • 条件等候是线程间同步的一种本领,如果只有一个线程,条件不满意,不停等下去都不会满意,以是必须要有一个线程通过某些利用,改变共享变量,使原先不满意的条件变得满意,而且友爱的关照等候在条件变量上的线程。
  • 条件不会无缘无故的突然变得满意了,一定会牵涉到共享数据的变化,以是一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据。
  • 当线程进入临界区时必要先加锁,然后判断内部资源的环境,若不满意当前线程的实验条件,则必要在该条件变量下举行等候,但此时该线程是拿着锁被挂起的,也就意味着这个锁再也不会被开释了,此时就会发生死锁题目。
  • 以是在调用pthread_cond_wait函数时,还必要将对应的互斥锁传入,此时当线程由于某些条件不满意必要在该条件变量下举行等候时,就会主动开释该互斥锁。
  • 当该线程被唤醒时,该线程会接着实验临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁,因此当某一个线程被唤醒时,现实会主动获得对应的互斥锁。
总结一下:


  • 等候的时间通常是在临界区内等候的,当该线程进入等候的时间,互斥锁会主动开释,而当该线程被唤醒时,又会主动获得对应的互斥锁,继承实验后续代码。
  • 条件变量必要共同互斥锁利用,此中条件变量是用来完成同步的,而互斥锁是用来完成互斥的(条件变量就是为了办理纯互斥场景下的饥饿题目)。
  • pthread_cond_wait函数有两个功能,一就是让线程在特定的条件变量下等候,二就是让线程开释对应的互斥锁。

   错误的计划
  你大概会想:当我们进入临界区上锁后,如果发现条件不满意,那我们先解锁,然后在该条件变量下举行等候不就行了。
  1. //错误的设计
  2. pthread_mutex_lock(&mutex);
  3. while (condition_is_false){
  4.         pthread_mutex_unlock(&mutex);
  5.         //解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
  6.         pthread_cond_wait(&cond);
  7.         pthread_mutex_lock(&mutex);
  8. }
  9. pthread_mutex_unlock(&mutex);
复制代码
但这是不可行的,由于解锁和等候不是原子利用,调用解锁之后,在调用pthread_cond_wait函数之前,如果已经有其他线程获取到互斥量,发现此时条件满意,于是发送了信号,那么此时pthread_cond_wait函数将错过这个信号,终极大概会导致线程永世不会被唤醒,因此解锁和等候必须是一个原子利用。
而现实进入pthread_cond_wait函数后,会先判断条件变量是否即是0,若即是0则阐明不满意,此时会先将对应的互斥锁解锁,直到pthread_cond_wait函数返回时再将条件变量改为1,并将对应的互斥锁加锁。
条件变量利用规范

等候条件变量的代码
  1. pthread_mutex_lock(&mutex);
  2. while (条件为假)
  3.         pthread_cond_wait(&cond, &mutex);
  4. 修改条件
  5. pthread_mutex_unlock(&mutex);
复制代码
唤醒等候线程的代码
  1. pthread_mutex_lock(&mutex);
  2. 设置条件为真
  3. pthread_cond_signal(&cond);
  4. pthread_mutex_unlock(&mutex);
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

×
登录参与点评抽奖,加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表