【Linux学习】线程互斥与同步

目录

二十.线程互斥

[20.1 什么是线程互斥?](#20.1 什么是线程互斥?)

[20.2 为什么需要线程互斥?](#20.2 为什么需要线程互斥?)

[20.3 互斥锁mutex](#20.3 互斥锁mutex)

[20.4 互斥量的接口](#20.4 互斥量的接口)

[20.4.1 互斥量初始](#20.4.1 互斥量初始)

[20.4.2 互斥量销毁](#20.4.2 互斥量销毁)

[20.4.3 互斥量加锁](#20.4.3 互斥量加锁)

[20.4.4 互斥量解锁](#20.4.4 互斥量解锁)

[20.4.5 互斥量的基本原理](#20.4.5 互斥量的基本原理)

[20.4.6 带上互斥锁后的抢票程序](#20.4.6 带上互斥锁后的抢票程序)

[20.5 死锁问题](#20.5 死锁问题)

死锁的四个必要条件

如何避免死锁

[20.6 互斥量的实现机制](#20.6 互斥量的实现机制)

二十一.线程同步

[21.1 同步概念与竞态条件](#21.1 同步概念与竞态条件)

[21.2 条件变量](#21.2 条件变量)

[21.2.1 条件变量初始](#21.2.1 条件变量初始)

[21.2.2 条件变量销毁](#21.2.2 条件变量销毁)

[21.2.3 等待满足](#21.2.3 等待满足)

[21.2.3 唤醒等待](#21.2.3 唤醒等待)

[21.3 利用条件变量实现线程同步](#21.3 利用条件变量实现线程同步)

[21.4 为什么pthread_cond_wait需要互斥量?](#21.4 为什么pthread_cond_wait需要互斥量?)

[21.5 条件变量使用规范](#21.5 条件变量使用规范)


二十.线程互斥

20.1 什么是线程互斥?

线程互斥是一种同步机制,用于控制对共享资源的访问,以确保在任意给定的时刻只有一个线程可以访问该资源。在多线程编程中,当多个线程同时竞争访问某个共享资源时,如果没有适当的同步机制,可能会导致竞争条件和数据不一致性的问题。线程互斥通过引入互斥锁等机制,使得在任意时刻只能有一个线程持有资源的访问权限,从而避免了竞争条件和数据不一致性的发生。

进程线程间的互斥相关背景概念

  • 临界资源: 多线程执行流共享的资源叫做临界资源。
  • 临界区: 每个线程内部,访问临界资源的代码,就叫做临界区。
  • **互斥:**任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  • 原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

20.2 为什么需要线程互斥?

这里我们直接举个栗子来回答这个问题,我们用代码来模拟一个抢票机制,这里的所定义的票数tickets就是所谓的临界资源,这里我们一共创建5个线程来模拟抢票程序,并不断打印时时监测抢票过程

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
 
int tickets = 1000;//定义一个全局变量,这就是临界资源,1000张票  
  
void* ThreadRotinue(void* args)  
{  
    int id = *(int*)args;  
    delete (int*)args;
  
    while(true)
    {  
        if(tickets > 0)
        {  
            usleep(10000); //usleep函数能把线程挂起一段时间, 单位是微秒(千分之一毫秒)。 
            printf("线程[%d] 抢票:%d\n", id, tickets);  
            tickets--;  //抢票,票数递减
        }  
        else
        {  
            break;  
        }  
    }                                                                                                                                                                 
}                                                                                                                                                 
                                                                                                                                                  
int main()                                                                                                                                        
{                                                                                                                                                 
    pthread_t tid[5];
                                                                                                                             
    for(int i = 0; i < 5; i++)//主线程创建出5个线程去抢票
    {                                                                                                                   
        int* id = new int(i);                                                                                            
        pthread_create(tid + i, nullptr, ThreadRotinue, id);                                                                                        
    }
                                                                                                                                             
    for(int i = 0; i < 5; i++)
    {                                                                                                                   
        pthread_join(tid[i], nullptr); //等待线程                                                                                                            
    }
                                                                                                                                             
    return 0;                                                                                                                                     
} 

运行结果如下:

这里我们惊讶的发现,结果竟然出现了票数剩余为负数的情况!

该代码中记录剩余票数的变量tickets就是临界资源,因为它被多个执行流同时访问,而判断tickets是否大于0、打印剩余票数以及--tickets这些代码就是临界区,因为这些代码对临界资源进行了访问。

分析剩余票数出现负数的原因:

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

为什么--ticket不是原子操作?

我们对一个变量进行--,我们实际需要进行以下三个步骤:

  1. Load(加载) :将共享变量 tickets 从内存加载到寄存器中。这一步确保了线程正在使用的是最新的变量值。

  2. Update(更新):在寄存器中执行减 1 操作。这意味着对寄存器中的值进行修改,而不是直接在内存中进行修改,以确保线程独占了这个操作。

  3. Store(存储) :将新的值从寄存器写回到共享变量 tickets 的内存地址。这样可以确保其他线程在需要访问该变量时,能够获取到更新后的值。

--操作对应的汇编代码如下 :

这个过程我们可以用下面几个图片形象表示 :

1.现在有两个线程thread1和thread2,thread1处于运行中、thread2等待中

当thread1把tickets的值读进CPU由于时间片耗尽被切走了,假设此时thread1读取到的值就是1000,而当thread1被切走时,寄存器中的1000叫做thread1的上下文信息,因此需要被保存起来,之后thread1就被挂起了,放到了等待队列。(也就是说thread1只进行了Load(加载)操作)

2.此时thread2被调度了,thread1处于等待中

此时thread2被调度了,由于thread1只进行了 Load(加载),此时thread2此时看到tickets的值还是1000,有可能系统给thread2的时间片较多,导致thread2一次性执行了100次完整的 --操作才被切走,最终tickets由1000减到了900。

3.thread2时间片耗尽被切走了,切到thread1带着上下文信息恢复

此时thread2时间片到了被挂起了,又切换到了thread1,它就带着上下文过来恢复,而他的上下文记录到它还处于刚刚完成对ticket的Load(加载)操作 ,此时寄存器中load的ticket值仍然是1000,这时它接着完成了Update(更新)操作 ,也就是对1000减到999,最后再然后再Store(存储)操作 将更新的ticket写回到内存中,此时内存中的值又由900变成了999


为了解决这个问题,这里我们引入互斥锁mutex的概念

20.3 互斥锁mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量成为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,就会带来一些问题。

要解决上述抢票系统的问题,需要做到三点:

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

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

20.4互斥量的接口

20.4.1 互斥量初始

在使用互斥量之前,需要对其进行初始化。一般使用 pthread_mutex_init 函数进行初始化,其原型如下:

cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

参数说明:

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

返回值说明:

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

调用pthread_mutex_init函数初始化互斥量叫做动态分配 ,除此之外,我们还可以用下面这种方式初始化互斥量,该方式叫做静态分配

cpp 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

20.4.2 互斥量销毁

在不再需要使用互斥量时,需要将其销毁以释放资源。一般使用 pthread_mutex_destroy 函数进行销毁,其原型如下:

cpp 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要销毁的互斥量。

返回值说明:

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

销毁互斥量需要注意:

  • 使用PTHREAD_MUTEX_INITIALIZER初始化(静态分配)的互斥量不需要销毁。
  • 不要销毁一个已经加锁的互斥量。
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

20.4.3 互斥量加锁

当线程需要访问临界资源时,需要先对互斥量加锁,以确保只有一个线程能够进入临界区。一般使用 pthread_mutex_lock 函数进行加锁,其原型如下:

cpp 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);

参数说明:

  • mutex:要加锁的互斥量

返回值说明:

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

调用pthread_mutex_lock时,可能会遇到以下情况:

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

20.4.4 互斥量解锁

当线程访问完临界资源后,需要对互斥量解锁,以允许其他线程进入临界区。一般使用 pthread_mutex_unlock 函数进行解锁,其原型如下:

cpp 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要解锁的互斥量。

返回值说明:

  • 互斥量解锁成功返回0,失败返回错误码。

20.4.5 互斥量的基本原理

  • 互斥量的初始化与销毁: 在使用互斥量之前,需要对其进行初始化,一般通过 pthread_mutex_init 函数实现。销毁互斥量时使用 pthread_mutex_destroy 函数。这些操作确保互斥量的正确性和可用性。

  • 互斥量的加锁与解锁: 当线程需要访问临界资源时,首先需要对互斥量进行加锁,以确保只有一个线程能够进入临界区。加锁使用 pthread_mutex_lock 函数,解锁则使用 pthread_mutex_unlock 函数。这些操作保证了临界资源的独占性。

引入互斥量后,当一个线程申请到锁进入临界区时,在其他线程看来该线程只有两种状态,要么没有申请锁,要么锁已经释放了,因为只有这两种状态对其他线程才是有意义的。

例如,图中线程1进入临界区后,在线程2、3、4看来,线程1要么没有申请锁,要么线程1已经将锁释放了,因为只有这两种状态对线程2、3、4才是有意义的,当线程2、3、4检测到其他状态时也就被阻塞了

此时对于线程2、3、4而言,它们就认为线程1的整个操作过程是原子的。

临界区内的线程可能进行线程切换吗?

临界区内的线程完全可能进行线程切换,但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。

其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。

20.4.6 带上互斥锁后的抢票程序

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

int tickets = 1000; // 定义一个全局变量,这就是临界资源,1000张票
pthread_mutex_t mutex; // 定义互斥锁

void* ThreadRotinue(void* args)
{
    int id = *(int*)args;
    delete (int*)args;

    while(true)
    {
        pthread_mutex_lock(&mutex); // 加锁
        if(tickets > 0)
        {
            usleep(10000); // usleep函数能把线程挂起一段时间,单位是微秒(千分之一毫秒)。
            printf("线程[%d] 抢票:%d\n", id, tickets);
            tickets--; // 抢票,票数递减
        }
        else
        {
            pthread_mutex_unlock(&mutex); // 解锁
            break;
        }
        pthread_mutex_unlock(&mutex); // 解锁
    }
}

int main()
{
    pthread_t tid[5];
    pthread_mutex_init(&mutex, NULL); // 初始化互斥锁

    for(int i = 0; i < 5; i++) // 主线程创建出5个线程去抢票
    {
        int* id = new int(i);
        pthread_create(tid + i, nullptr, ThreadRotinue, id);
    }

    for(int i = 0; i < 5; i++)
    {
        pthread_join(tid[i], nullptr); // 等待线程
    }

    pthread_mutex_destroy(&mutex); // 销毁互斥锁

    return 0;
}

演示效果:

20.5 死锁问题

死锁是多线程编程中常见的问题,指的是两个或多个线程相互等待对方持有的资源,导致所有线程都无法继续执行的状态。 在使用互斥锁时,如果不注意锁的加锁顺序,就容易导致死锁问题。

这里我们举一个经典的死锁例子:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

mutex mutex1;
mutex mutex2;

void threadFunction1() {
    lock_guard<mutex> lock1(mutex1);
    this_thread::sleep_for(chrono::milliseconds(100));
    lock_guard<mutex> lock2(mutex2);
    cout << "Thread 1 acquired mutex1 and mutex2" << endl;
}

void threadFunction2() {
    lock_guard<mutex> lock2(mutex2);
    this_thread::sleep_for(chrono::milliseconds(100));
    lock_guard<mutex> lock1(mutex1);
    cout << "Thread 2 acquired mutex2 and mutex1" << endl;
}

int main() {
    thread t1(threadFunction1);
    thread t2(threadFunction2);

    t1.join();
    t2.join();

    return 0;
}

运行代码后:

可以观察到,此时程序就处于一个被阻塞的状态

用ps命令查看该进程时可以看到,该进程当前的状态是Sl+,其中的l实际上就是lock的意思,表示该进程当前处于一种死锁的状态。

  • 在这个示例中,有两个线程,每个线程都试图先锁定 mutex1,然后再锁定 mutex2。当一个线程已经锁定了 mutex1,而另一个线程已经锁定了 mutex2,那么它们都会等待对方释放对方所持有的互斥量。这种情况下就可能发生死锁。
  • 例如,线程1获得了 mutex1 的锁,然后暂停,线程2获得了 mutex2 的锁,然后暂停。接下来,线程1试图获取 mutex2 的锁,但由于线程2已经持有了 mutex2 的锁,因此线程1会被阻塞。同样的,线程2也试图获取 mutex1 的锁,但由于线程1已经持有了 mutex1 的锁,因此线程2也会被阻塞。这样,两个线程就会相互等待,导致死锁。
  • 为了避免这种死锁,我们应该保持一致的锁定顺序。例如,可以约定所有线程都先锁定 mutex1,然后再锁定 mutex2。

死锁的四个必要条件

  1. 互斥条件: 一个资源每次只能被一个执行流使用。
  2. 请求与保持条件: 一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件: 一个执行流已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系。

注意: 这是死锁的四个必要条件,也就是说只有同时满足了这四个条件才可能产生死锁。

如何避免死锁

  1. 加锁顺序:对多个互斥量加锁时,保持一致的加锁顺序,避免不同线程以不同的顺序加锁而导致死锁。
  2. 加锁时间:尽量减小临界区的范围,在持有锁的时间内,尽快完成对资源的操作。
  3. 超时机制:在获取锁的时候设置超时,如果超过一定时间仍未获得锁,则放弃获取资源。
  4. 避免嵌套锁:尽量避免在一个互斥区域内再次申请其他锁,以免造成死锁。

除此之外,还有一些避免死锁的算法,比如死锁检测算法和银行家算法。

20.6互斥量的实现机制

锁是否需要被保护?

我们说被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。所有的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个执行流共享的资源,也就是说锁本身就是临界资源。

既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?

锁实际上是自己保护自己的,我们只需要保证申请锁的过程是原子的,那么锁就是安全的。

如何实现申请锁的过程是原子的?

  • 上面我们已经说明了--++操作不是原子操作,可能会导致数据不一致问题。
  • 要保证申请锁的过程是原子的,通常使用底层的硬件指令来实现。大多数体系结构提供了一种原子交换指令,如 xchg 或 exchange 指令。这些指令可以在一个操作中完成寄存器和内存单元之间的数据交换,保证了这个操作的原子性。因此,申请锁的过程可以通过这些原子交换指令来实现,确保在任何时候只有一个线程能够成功地获取锁。

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

%al是一个cpu上的寄存器,xchgb是交换指令

我们创建锁,本质是在内存上创建一个变量,初始化锁是将锁的初始化为一个非0的值。

例如,此时内存中mutex的值为1,thread1申请锁时先将al寄存器中的值设为0,然后将al寄存器中的值与内存中mutex的值进行交换。

交换完成后检测该线程的al寄存器中的值为1,则该线程申请锁成功,可以进入临界区对临界资源进行访问。

而此后的thread2若是再申请锁,与内存中的mutex交换得到的值就是0了,此时该线程申请锁失败,需要被挂起等待,直到锁被释放后再次竞争申请锁

二十一.线程同步

21.1 同步概念与竞态条件

  • **同步概念:**同步是指在多线程环境下,协调不同线程之间的执行顺序和操作,以确保它们能够按照预期的顺序执行和相互协作。在多线程编程中,同步用于解决竞争条件和数据一致性的问题,确保线程之间的协作正确可靠。
  • 竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件。

举个生活例子:

同步就是操作过程中必须要有先后,比如妈妈做完饭后,儿子才能开始吃饭。一家人到齐后才能吃饭。


  • 首先需要明确的是,单纯的加锁是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后什么也不做,所以在我们看来这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题。
  • 单纯的加锁是没有错的,它能够保证在同一时间只有一个线程进入临界区,但它没有高效的让每一个线程使用这份临界资源。
  • 现在我们增加一个规则,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后。
  • 增加这个规则之后,下一个获取到锁的资源的线程就一定是在资源等待队列首部的线程,如果有十个线程,此时我们就能够让这十个线程按照某种次序进行临界资源的访问。

21.2 条件变量

条件变量是一种线程同步机制,用于在多个线程之间进行协调和通信。条件变量通常与互斥锁结合使用,用于等待某个条件的发生。当条件不满足时,线程可以调用条件变量的等待操作来等待条件的发生,同时释放互斥锁,让其他线程能够进入临界区。当条件满足时,线程可以调用条件变量的通知操作来通知等待的线程条件已经满足,从而唤醒等待的线程继续执行。

21.2.1 条件变量初始

初始化分为两种:

cpp 复制代码
//动态分配
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
//静态分配
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

参数说明:

  • **cond:**需要初始化的条件变量。
  • **attr:**初始化条件变量的属性,一般设置为NULL。

返回值说明:

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

21.2.2 条件变量销毁

条件变量的销毁可以使用 pthread_cond_destroy 函数,用于释放条件变量占用的资源。

cpp 复制代码
int pthread_cond_destroy(pthread_cond_t *cond);

参数说明:

  • **cond:**需要销毁的条件变量。

返回值说明:

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

使用静态分配初始化的条件变量不需要销毁;

21.2.3 等待满足

线程可以使用条件变量的等待操作来等待条件的发生。等待操作通常与互斥锁一起使用,以确保等待操作的原子性。

cpp 复制代码
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

参数说明:

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

返回值说明:

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

21.2.3 唤醒等待

条件变量的通知操作用于唤醒等待条件的线程。有两种通知方式:

唤醒单个等待线程 :使用 pthread_cond_signal 函数。

cpp 复制代码
int pthread_cond_signal(pthread_cond_t *cond);

唤醒全部等待线程 :使用 pthread_cond_broadcast 函数。

cpp 复制代码
int pthread_cond_broadcast(pthread_cond_t *cond);

参数说明:

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

返回值说明:

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

21.3 利用条件变量实现线程同步

cpp 复制代码
#include <iostream>    
#include <pthread.h>    
#include <string>    
#include <unistd.h>    

using namespace std;    

#define NUM 5    

pthread_mutex_t mtx;    
pthread_cond_t cond;    
int tickets = 100;

void* producer(void* args)    
{    
    string name = (char*)args;    
    while (tickets > 0)    
    {       
        pthread_mutex_lock(&mtx); // 加锁
        // 生产者线程在唤醒消费者线程之前修改共享资源
        pthread_cond_signal(&cond);//1.唤醒在条件变量下一个线程  
        pthread_mutex_unlock(&mtx); // 解锁
        sleep(1);    
    }    
}    

void* buyer(void* args)    
{    
    int id = *(int*)args;    
    delete (int*)args;    

    pthread_mutex_lock(&mtx); // 加锁
    while (tickets > 0) {
        // 消费者线程在循环中等待条件变量的信号
        pthread_cond_wait(&cond, &mtx); // 等待唤醒
        if (tickets > 0) {
            cout << "线程[" << id << "] 抢到票:" << tickets << endl;
            tickets--; // 抢票,票数递减
        }
    }
    pthread_mutex_unlock(&mtx); // 解锁

    return NULL;
}

int main()
{
    pthread_mutex_init(&mtx, nullptr);
    pthread_cond_init(&cond, nullptr);
 
    pthread_t master; // 创建生产者线程
    pthread_t worker[NUM]; // 创建消费者线程数组
 
    pthread_create(&master, nullptr, producer, (void*)"boss");
    
    for(int i = 0; i < NUM; i++)
    {
        int* num = new int(i);
        pthread_create(worker + i, nullptr, buyer, (void*)num);
    }
    
    for(int i = 0; i < NUM; i++)
    {
        pthread_join(worker[i], nullptr);
    }
    
    pthread_join(master, nullptr);
 
    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);
    
    return 0;
}                                                                                                                                                                  

此时我们会发现这五个线程时具有明显的顺序性,这是因为这5个线程启动时默认都会在该条件变量下去等待,而我们每次都唤醒的是在当前条件变量下等待的第一个线程,当该线程执行完打印操作后会继续排到等待队列的尾部进行等待,所以我们能够看到一个轮换的现象。 这样就实现了线程的同步

21.4 为什么pthread_cond_wait需要互斥量?

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

按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:

cpp 复制代码
//错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false){
	pthread_mutex_unlock(&mutex);
	//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
	pthread_cond_wait(&cond);
	pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);

调用解锁之后,在调用pthread_cond_wait函数之前,如果已经有其他线程获取到互斥量,发现此时条件满足,于是发送了信号,那么此时pthread_cond_wait函数将错过这个信号,最终可能会导致线程永远不会被唤醒

21.5 条件变量使用规范

等待条件变量的代码

cpp 复制代码
pthread_mutex_lock(&mutex);
while (条件为假)
	pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);

唤醒等待线程的代码

cpp 复制代码
pthread_mutex_lock(&mutex);
//设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
相关推荐
wowocpp4 分钟前
ubuntu 22.04 硬件配置 查看 显卡
linux·运维·ubuntu
山河君16 分钟前
ubuntu使用DeepSpeech进行语音识别(包含交叉编译)
linux·ubuntu·语音识别
鹏大师运维20 分钟前
【功能介绍】信创终端系统上各WPS版本的授权差异
linux·wps·授权·麒麟·国产操作系统·1024程序员节·统信uos
筱源源23 分钟前
Elasticsearch-linux环境部署
linux·elasticsearch
龙哥说跨境1 小时前
如何利用指纹浏览器爬虫绕过Cloudflare的防护?
服务器·网络·python·网络爬虫
monkey_meng1 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马1 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng1 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
pk_xz1234562 小时前
Shell 脚本中变量和字符串的入门介绍
linux·运维·服务器
小珑也要变强2 小时前
Linux之sed命令详解
linux·运维·服务器