【Linux】线程同步与互斥

文章目录

  • [📖 前言](#📖 前言)
  • [1. 线程互斥](#1. 线程互斥)
    • [1.1 临界资源:](#1.1 临界资源:)
    • [1.2 互斥性与原子性:](#1.2 互斥性与原子性:)
      • [1.2 - 1 概念回顾](#1.2 - 1 概念回顾)
    • [1.3 线程安全:](#1.3 线程安全:)
      • [1.3 - 1 可重入与不可重入](#1.3 - 1 可重入与不可重入)
    • [1.4 线程加锁与解锁:](#1.4 线程加锁与解锁:)
      • [1.4 - 1 竞争锁](#1.4 - 1 竞争锁)
      • [1.4 - 2 锁的原子性](#1.4 - 2 锁的原子性)
    • [1.5 加锁的原子性如何实现:](#1.5 加锁的原子性如何实现:)
    • [1.6 死锁:](#1.6 死锁:)
      • [1.6 - 1 死锁的演示](#1.6 - 1 死锁的演示)
      • [1.6 - 2 一把锁出现死锁的情况](#1.6 - 2 一把锁出现死锁的情况)
      • [1.6 - 3 死锁的条件](#1.6 - 3 死锁的条件)
  • [2. 线程同步](#2. 线程同步)
    • [2.1 什么是同步:](#2.1 什么是同步:)
    • [2.2 条件变量:](#2.2 条件变量:)
      • [2.2 - 1 pthread_cond_init / destroy](#2.2 - 1 pthread_cond_init / destroy)
      • [2.2 - 2 pthread_cond_wait](#2.2 - 2 pthread_cond_wait)
      • [2.2 - 3 唤醒线程](#2.2 - 3 唤醒线程)
    • [2.3 代码演示:](#2.3 代码演示:)
      • [2.3 - 1 同步的现象](#2.3 - 1 同步的现象)
      • [2.3 - 2 解决小bug](#2.3 - 2 解决小bug)
      • [2.3 - 4 条件变量经典错误](#2.3 - 4 条件变量经典错误)

📖 前言

上一章我们学习了线程的控制,理解了什么是线程id,和线程私有栈等相关的概念,还学习了线程退出的四种方式,并学习了几个相关接口。

本章我们还是继续进行线程方面的学习,接下来我们将学线程的互斥加锁,同步条件变量等待等相关问题的学习,目标已经确定,搬好小板凳准备开讲啦......


1. 线程互斥

1.1 临界资源:

在之前【进程通信 --- 共享内存】的学习中,我们已经介绍过临界资源,临界区,原子性等概念了,今天我们再来复盘一下:

  • 临界资源: 被多个进程/线程(执行流)都能够看到并访问的资源叫做临界资源
  • 如果没有对临界资源进行任何保护,直接对于临界资源的访问。
  • 多个进程/线程(执行流)在进行访问的时候,就都是乱序的。
  • 可能会因为读写交叉而导致的各种乱码、废弃数据、访问控制方面的问题!!
  • 临界资源有安全的也有不安全的,取决于内部是否做了保护。
  • 临界区: 对多个进程/线程(执行流)而言,访问临界资源的代码
  • 我的进程/线程代码中,有大量的代码,只有一部分代码,会访问临界资源。
  • 多个进程/线程对临界资源做读写的代码,我们称之为临界区。

1.2 互斥性与原子性:

1.2 - 1 概念回顾

  • 原子性: 我们把一件事情,要不没做,要么做完了,叫原子性(没有中间状态)。
  • 互斥: 任何时刻,只允许一个进程/线程,访问临界资源。

在线程中,存在着访问临界资源而导致的冲突:

如果我们要对一个变量进行++/- -要做什么工作呢?

  • 假设是对100进行 - - 操作。
    • 要先将100从内存拷贝到CPU里面。
    • 然后在CPU里面做好计算。
    • 最后从CPU中再拷贝内存中。

补充:

CPU内的寄存器是被所有的执行流共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。线程被切换的时候,需要保存上下文,线程被换回的时候,需要恢复上下文。

线程切换时,需要保存的不是寄存器,而是寄存器里面的数据。

所以谈线程必谈两个概念,一个是线程的上下文,另一个是线程的独立站结构。

问题出现:

  • 假设第一个进程将数据100拷贝到CPU中进行- -操作。
  • 此时线程替换,切换到第二个线程。
  • 第二个线程再将数据从内存总拷贝到CPU,执行其他操作(连续- -50)。
  • 若第二个线程将数据从100减到50,再切回原来的进程。
  • 原来的进程再继续执行,直接把第二个线程好不容易减到50的值又干到了99

为了保证能够正确的控制线程的访问,其就必须维护自身的原子性!不能有中间状态!!

当我们访问某种资源的时候,任何时刻都只有一个执行流在进行访问,这个就叫做:互斥特性。

为了维护互斥性,我们要给线程的临界资区加锁。

1.3 线程安全:

线程安全是指在多线程环境下,对共享资源的访问不会导致数据不一致或者出现意料之外的结果。

当多个线程同时访问共享资源时,如果没有适当的同步机制或保护措施,可能会导致以下问题:

  1. 竞态条件(Race Condition):多个线程对同一资源进行读写操作,由于执行顺序不确定,可能导致结果的不确定性、错误的计算结果或数据丢失等问题。
  2. 数据竞争(Data Race):多个线程同时对同一数据进行读写操作,由于缺乏同步机制,可能导致数据的不一致性或错误的结果。

为了确保线程安全,需要采取合适的并发控制措施,如加锁机制、原子操作、信号量等。这些机制可以保证在任意时刻只有一个线程能够访问共享资源,避免数据竞争和竞态条件的发生。

1.3 - 1 可重入与不可重入

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

面对不可重入函数,这不是问题,这是种特性,一般通过加锁来解决。

加锁本质上是把多线程串行起来,确保同一时间只有一个线程可以进入临界区,让每个线程可以安全的调用这个不可重入的函数。

如果一个库函数明明告知你了是不可重入的,但是还不加保护的在多线程操作中调用它。

那么这段代码如果出现bug,并不是库函数本身有问题,是编码的问题。

注意:

  • 重入是种现象,不是问题(特性)。
  • 一个函数被重复进入,叫做被重入。
  • 在被重复进入的情况下,执行代码出了问题,叫做该函数是不可重入函数。
  • 如果没有出现问题,叫做可重入。

一个线程对全局变量的恶意修改 ,可能会影响其他线程安全。

一个线程有bug导致线程退出,导致其他线程也退出,也叫做线程退出。

绝大多数的系统自带的库(比如C++的STL库)都是不可重入的,并非所有容器都是线程安全的。

1.4 线程加锁与解锁:

  • 加锁:

定义全局的互斥锁,所有线程能访问:


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

方法一:静态分配

cpp 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法二:动态分配

cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 参数:
// mutex:要初始化的互斥量
// attr:NULL
  • 解锁 / 销毁锁:


销毁互斥量需要注意:

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

1.4 - 1 竞争锁

多线程竞争锁:

  • 如果多个线程同时对资源进行读写操作,可能会引发竞争条件(Race Condition),导致数据不一致或其他问题。
  • 为了避免竞争条件,线程必须在访问共享资源之前先获取互斥锁。
  • 当一个线程获得互斥锁后,其他线程就无法再获取该锁,只能等待锁被释放。
  • 这样可以确保每次只有一个线程访问共享资源,保证数据的一致性和正确性。

因此,线程互斥锁的使用涉及到多个线程之间的竞争获取锁的过程,以确保同一时间只有一个线程能够获得互斥锁,并访问共享资源。

其他线程已经锁定互斥量(互斥锁),或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

我们用多线程实现一个简单的抢票程序:

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

using namespace std;

// int 票数计数器
// 临界资源
int tickets = 10000; // 临界资源,可能会因为共同访问,可能会造成数据不一致问题。

// 定义一个全局的锁
pthread_mutex_t mutex;

void* getTickets(void* args)
{
    const char* name = static_cast<const char*>(args);

    while (true)
    {
        // 加锁
        pthread_mutex_lock(&mutex);
        if (tickets > 0)
        {
            usleep(1000);
            cout << name << " 抢到了票, 票的编号: " << tickets << endl;
            tickets--;

            // 解锁
            pthread_mutex_unlock(&mutex);

            //other code
            usleep(123); //模拟其他业务逻辑的执行
        }
        else
        {
            // 票抢到几张,就算没有了呢?0
            cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
            
            // 解锁
            pthread_mutex_unlock(&mutex);
            break;
        }
    }

    return nullptr;
}

int main()
{
    // 对锁初始化
    pthread_mutex_init(&mutex, nullptr);

    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;

    pthread_create(&tid1, nullptr, getTickets, (void*)"thread1");
    pthread_create(&tid2, nullptr, getTickets, (void*)"thread2");
    pthread_create(&tid3, nullptr, getTickets, (void*)"thread3");

    int n = pthread_join(tid1, nullptr);
    cout << n << ":" << strerror(n) << endl;
    pthread_join(tid2, nullptr);
    cout << n << ":" << strerror(n) << endl;
    pthread_join(tid3, nullptr);
    cout << n << ":" << strerror(n) << endl;

    // 锁用完了,释放这把锁
    pthread_mutex_destroy(&mutex);

    return 0;
}

如果不加锁的话,if条件判断是否也会有线程安全问题:

  • if判断的时候也有可能出现问题,因为CPU也需要参与。
    • 假设票数是1,线程A还没开始减减的时候,就被切走了。
    • 线程B来了,也要从内存读到CPU里,判断票数为1,线程B也要减减。
    • 两个线程都要减减,就会出现将票减到负数的情况。
  • 注意:当执行完if条件判断后,CPU会将tickets变量的值写回内存,然后再执行tickets - - 操作时,会从内存中读取最新的tickets值进行减减操作。

所以就导致了多线程进行抢票的时候出现了负数票的情况:

加锁的范围:

  • 临界区,只要对临界区加锁,而且加锁的粒度越细越好。
  • 加锁的本质是让线程执行临界区代码串行化,不能对所有代码加锁,只能对临界区加锁(加锁要短小惊叹)。
  • 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加。
  • 锁保护的是临界区,任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!

1.4 - 2 锁的原子性

这把锁,本身不就也是临界资源吗,谁来给它加锁呢?锁的设计者早就想到了~

  • pthread_mutex_lock竞争和申请锁的过程,就是原子的!

难度在加锁的临界区里面,就没有线程切换了吗??

  • 我在临界资源对应的临界区中就锁了,还是是多行代码,还可以被切换!
  • 线程被切走,是完全可以,因为线程执行的加锁解锁等对应的也是代码。
  • 线程在任意代码出都可以被切换!!
  • 但是线程加锁是原子的,最后的结果无非就是:要么拿到了锁,要么没有拿到。
  • 当多个线程竞争锁的时候,不存在中间状态,不存在锁被拿了一半。

当加锁的线程被切走的时候,绝不会有其他线程进入临界区!!

  • 每个线程进入临界区都必须先申请锁!!
  • 假如当前的锁,被线程A申请走了。
  • 即便当前的线程A没有被调度(它是从CPU切走了),但是它是抱着锁走的!!
  • 一旦一个线程持有了锁,该线程根本就不担心任何的切换问题!
  • 对于其他线程而言,线程A访问临界区,只有没有进入和使用完毕两种状态,才对其他线程有意义!

所有线程要想进入临界区,就得申请锁,只要有了锁才能进入临界区。

当一个线程有了锁,就不害怕被切换了。

进入临界区前,都必须要申请锁,当一个线程申请锁之后,另一个线程只能等,要么使用临界区使用完了,要么根本就没有进入临界区。

在执行临界区代码时,加锁的那部分代码,会不会切换呢?会的!或者阻塞呢?会的!或者挂起呢?会的!

1.5 加锁的原子性如何实现:

为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。

现在我们看一下pthread_mutex_lockpthread_mutex_unlock的伪代码:

  • %al:寄存器在CPU内。
  • mutex:内存中的一个变量,默认值是1。
  • 寄存器的值,和内存的值,用一条语句做交换。
  • 寄存器的内容如果大于0:
    • 则代表申请锁成功。
  • 寄存器的内容如果小于0:
    • 就等待挂起。
  • 凡是在寄存器中的数据,全部都是线程的内部上下文!!
  • 多个线程看起来同时在访问寄存器,但是互不影响。

寄存器只有一个,是被所有线程共享的,但是寄存器里的内容是被所有线程私有的。单CPU任何时刻,只能有一个线程在跑。

每个线程要切走时,必须将自己的上下文带走。

过程:

  • 线程A申请锁:
    • 线程A要申请锁,先执行将0写入到寄存器当中。
    • 然后将%a寄存器中的0和内存中mutex的值交换。
    • 此时%a寄存器中的值是1,然后线程A再被切走。
    • 切走后,寄存器内的值会被保存在线程的上下文中一并被带走。
  • 线程B申请锁:
    • 线程B来的时候,线程A必须从CPU上剥离下去,剥离走就必须将上下文带走。
    • 线程B再来执行加锁代码。
    • 线程B再将0写到寄存器当中。
    • 然后将%a寄存器中的0和内存中mutex的值交换。
    • 此时内存中mutex的值是0,这是时候就相当于0和0的交换。
    • %a寄存器的内容不大于0,线程B挂起等待。
    • 此时线程B就叫申请锁失败了。

用一条汇编的方式,把寄存器的值放到mutex,把mutex的值放到了寄存器,这个动作就叫做加锁。

本质:

  • 将数据从内存读入寄存器,本质是将数据从共享变成线程私有!
  • 将自己的锁拿到线程自己的上下文里。
  • 只要交换到了,以后还有线程想申请锁就不可能拿到了。

解锁:解锁,就是把1再写回去mutex,这就完成了解锁。

1.6 死锁:

1.6 - 1 死锁的演示

模拟死锁,出现的情况:

  • 两个线程拿着对方要的锁,自己又抱着一把锁。
  • 线程1拿着A,线程2拿着B,但是这两个线程又都向对方要锁。
  • 互相申请对方的锁,但是自己要的锁已经被对方拿走了。

现象:

  • 线程1申请B锁的时候,锁被线程2拿着了,申请不到,这个线程1就被挂起了(抱着A挂起的)。
  • 线程2申请A锁的时候,锁被线程1拿着了,申请不到,这个线程2就被挂起了(抱着B挂起的)。
  • 这就导致了死锁的问题。
cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include "Lock.hpp"

using namespace std;

// 模拟死锁

// 静态定义锁的方式(可以不用再去destroy,也可以不用对其进行init)
pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;

void *startRoutine1(void *args)
{
    while (true)
    {
        pthread_mutex_lock(&mutexA);
        sleep(1);
        pthread_mutex_lock(&mutexB);

        cout << "我是线程1,我的tid: " << pthread_self() << endl;

        pthread_mutex_unlock(&mutexA);
        pthread_mutex_unlock(&mutexB);
    }
}

void *startRoutine2(void *args)
{
    while (true)
    {
        pthread_mutex_lock(&mutexB);
        sleep(1);
        pthread_mutex_lock(&mutexA);

        cout << "我是线程2,我的tid: " << pthread_self() << endl;

        pthread_mutex_unlock(&mutexB);
        pthread_mutex_unlock(&mutexA);
    }
}

int main()
{
    pthread_t t1, t2;

    pthread_create(&t1, nullptr, startRoutine1, nullptr);
    pthread_create(&t2, nullptr, startRoutine2, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);

    return 0;
}

没有结果一直在阻塞中:

循环查看一下线程:

1.6 - 2 一把锁出现死锁的情况

一把锁会不会有死锁问题呢?我们来演示一下:

cpp 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int cnt = 100;

using namespace std;

void *startRoutine(void *args)
{
    string name = static_cast<char *>(args);
    while (true)
    {
        pthread_mutex_lock(&mutex);
        cout << name << " count : " << cnt-- << endl;

        // 代码写错了,写了两个锁
        // pthread_mutex_lock(&mutex);
        pthread_mutex_unlock(&mutex);
        
        // 耗时的操作尽量不要再临界区里面
        sleep(1);
    }
}

编码错误引起的:

那就是同时申请了一把锁两次,这就导致第二次申请锁的时候一直在等待第一把锁释放,这就导致了死锁问题。

1.6 - 3 死锁的条件

死锁的条件通常被称为"死锁四个必要条件",它们是:

  1. 互斥条件:至少有一个资源同时只能被一个线程(或进程)占用,不能同时被多个线程访问。
  2. 请求与保持条件:一个线程在持有一个资源的同时,又请求获取其他的资源。
  3. 不可剥夺条件:已分配的资源无法被强制性地抢占,只能由占有它的线程主动释放。
  4. 循环等待条件:存在一组线程,每个线程都在等待下一个线程所持有的资源,形成一个循环等待链。

当以上四种条件同时满足时,就可能发生死锁。要解决死锁问题,需要破坏其中至少一个条件。例如,可以采用资源有序分配、避免持有并等待、引入抢占机制以及打破循环等待等方法来预防和解除死锁。


2. 线程同步

2.1 什么是同步:

线程互斥,它是对的,难道它任何场景都是合理的吗?很显然并不是。

互斥有可能导致饥饿问题:

  • 一个执行流,长时间得不到某种资源。
  • 例如一个线程的优先级很高,每次竞争锁时,都可以优先竞争到。
  • 这就导致了其他线程竞争不到锁,造成了饥饿问题。

在保证临界资源安全的前提下(互斥等),让线程访问某种资源,具有一定的顺序性,称之为同步!

  • 防止饥饿线程协同。
  • 同步和互斥不是对立的关系,而是互相补充的关系。
  • 互斥保证了数据的安全,同步不一定需要互斥。

一般在保证互斥前提条件下,多做了一个工作,让多个线程访问某种资源具备了一定的顺序性,这种特性我们称之为,同步。

2.2 条件变量:

如何完成同步呢?

Linux中提供了完成同步的重要机制,叫做:条件变量。(最常用,没有之一,最常用的线程同步的策略)

条件变量:

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。
  • 这种情况就需要用到条件变量。

条件变量要和mutex互斥锁,一并使用!

2.2 - 1 pthread_cond_init / destroy

基本的接口和pthread库的其他接口很相似,用法也几乎一样。其中attr也是设置条件变量的属性,这里置为nullptr即可。

2.2 - 2 pthread_cond_wait

  • 如上两个接口都是让线程在一个条件变量下进行等待。
  • 其中timewait接口可以在条件变量下等,设置等待的时间(超时了就不等了)。
  • 条件变量也是临界资源,所以这里需要一个mutex互斥锁来保证条件变量读写的原子性。

2.2 - 3 唤醒线程


broadcast是给在当前条件变量等待的所有线程发信号唤醒,而signal是发送信号只唤醒一个线程。

条件变量决定,什么时候叫醒一个线程,以前只要有锁,如果所有线程都被叫醒,大家都去参与竞争,谁抢到了算谁的,这个机制完全是由调度器决定的。

2.3 代码演示:

下面的代码可以很好的演示上述的函数接口:

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <pthread.h>

using namespace std;

// 定义一个条件变量
pthread_cond_t cond;

// 定义一个互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 当前不用,但是接口需要,所以我们需要留下来

// 定义全局退出变量
volatile bool quit = false;

// 三个线程都会调用这个函数
void *waitCommand(void *args)
{
    string name = static_cast<char*>(args);
    
    pthread_mutex_lock(&mutex);
    // 如果不退出一直去运行
    while (!quit)
    {
        // pthread_cond_wait内部先解锁
        pthread_cond_wait(&cond, &mutex);
        // 被唤醒出来之后锁已经加上了

        cout << "thread id: " << pthread_self() << " running... " << name << endl;
    }
    pthread_mutex_unlock(&mutex);
    cout << "thread quit..." << (char*)args << endl;

    return nullptr;
}

int main()
{
    // 初始化一个条件变量
    pthread_cond_init(&cond, nullptr);

    // 创建三个线程
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, waitCommand, (void*)"thread1");
    pthread_create(&t2, nullptr, waitCommand, (void*)"thread2");
    pthread_create(&t3, nullptr, waitCommand, (void*)"thread3");

    // 主线程控制
    while(true)
    {
        char n;

        // cin和cout在交叉使用的时候,缓冲区会做强制刷新
        cout << "请输入你的command: ";
        cin >> n;

        if(n == 'n') 
        {
            // 唤醒在特定条件变量下等的线程
            pthread_cond_signal(&cond); 
        }
        else if (n == 'x')
        {
            pthread_cond_broadcast(&cond);
        }
        else
        {
            quit = true;
            break;
        }
        
        usleep(100);
    }
    
    // 唤醒所有等待的条件变量
    pthread_cond_broadcast(&cond);
    cout << "main thread quit..." << endl;

    // 释放条件变量
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    // 等待线程
    int m = pthread_join(t1, nullptr);
    cout << strerror(m) << endl;
    m = pthread_join(t2, nullptr);
    cout << strerror(m) << endl;
    m = pthread_join(t3, nullptr);
    cout << strerror(m) << endl;

    return 0;
}

运行结果:

2.3 - 1 同步的现象

现象:

  • 我们看到,如果一个一个唤醒时,所有线程是以队列的形式在排队的,当在排队的时候,明显能看到,执行时有一定的顺序性的。
  • 当我们将全部线程都唤醒时,发现所有等待的线程全部都被唤醒并运行起来。

但是我们退出时,将quit改成true,循环条件不满足,三个线程退出,将三个线程全部唤醒,我们却发现,阻塞等待住了,这是什么原因:(重点)

  • 判断一个条件是否满足,一定是在临界区内部才能得出条件是否满足,进而才能决定是否要挂起等待还是继续运行。
  • 所以一定是占有锁的情况。
  • 一旦占有锁了,把锁占了但是又去条件变量下去等了,那么谁来释放这个锁呢?
  • 该线程拿着锁去休眠了,导致其他线程无法申请该锁,这就有问题了。

pthread_cond_wait内部已经帮我们想到了这一点,并做了相应的措施:(重点)

cpp 复制代码
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)
{
	pthread_mutex_unlock(mutex);// 先解锁
    // 避免因为该线程拿着锁去休眠了,导致其他线程无法申请该锁
  
	//条件变量相关代码
	
    pthread_mutex_lock(mutex);// 条件满足后,再加锁
}

pthread_cond_wait内部是先调用pthread_mutex_unlock解锁,再调用pthread_mutex_lock加锁。

知道了pthread_cond_wait内部细节,再来回头看阻塞的原因:(重点)

  • 因为有多个线程,当被同时唤醒的时候,pthread_cond_wait内部都要重新加锁,就要同时去竞争一把锁,但是只有一个能竞争成功。
  • 最后其他线程竞争不到锁,就在锁那里被阻塞了,所以看到了只有一个线程退出成功了。
  • 那么其他线程就没有办法去退出了(一直卡在pthread_cond_wait函数最后加锁那里,等着申请锁),而且那个线程退出了也没有解锁,就导致其他线程无法占用这个锁,就一直在阻塞等待锁。
  • 最后就导致其他线程无法退出。

2.3 - 2 解决小bug

解决问题:在退出循环后解锁(重点)

  • 在退出循环后解锁,就可以很好的解决问题,使得每个线程都能退出。
    • 当我们输入q之后,false被改成true,但是此时所有线程还是在条件变量中等。
    • 当主线程将所有的在条件变量中等待的新线程都唤醒了,pthread_cond_broadcast之后。
    • 所有的线程都醒了过来,都要去加锁,只有一个线程抢到了锁。
    • 此时继续往下执行,循环条件不成立,退出循环然后解锁。
    • 解完锁之后,剩下的线程还处于抢锁的状态。
    • 然后剩下的线程中,有一个拿到了锁,继续上述过程,退出循环然后解锁。
    • 然后剩下的线程中,有一个拿到了锁,......
    • 直到所有线程都退出。

为什么主线程输入n多个线程会挨个执行?

  • 因为多个线程同时跑一起来之后,同时去申请了锁。
  • 只有一个能抢得到锁,其余线程都在申请锁那里阻塞住了。
  • 然后抢到锁的线程,条件等待,pthread_cond_wait内部解锁了。
  • 然后剩下的线程再去抢锁,其余线程都在申请锁那里阻塞住了。
  • 然后抢到锁的线程,条件等待,pthread_cond_wait内部解锁了。
  • ......
  • 直到最后所有的线程都已经在条件变量中等了。
  • 主线程输入npthread_cond_signal唤醒一个线程,然后加锁。
  • 该线程再次拿到了锁(因为其他线程都在条件变量里等了,没有线程和这个线程强锁),执行剩余代码(打印)。
  • 该线程while循环判断,再次进入循环,再次进入条件变量等待,pthread_cond_wait内部解锁了,线程继续等待。
  • 主线程中的pthread_cond_signal执行完毕,主线程的while循环再次执行。
  • 再次输入npthread_cond_signal唤醒另一个线程,具体唤醒哪一个,我也不知道。
  • 当多个线程同时等待同一个条件变量时,它们可能会被挨个唤醒。具体哪个线程先被唤醒取决于操作系统的调度机制。
  • 唤醒之后,该拿到了锁,继续上述过程......

2.3 - 4 条件变量经典错误

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 将错过这个信号。
  • 可能会导致线程永远阻塞在这个pthread_cond_wait 。
  • 所以解锁和等待必须是一个原子操作。

当一个线程解锁互斥量之后,另一个线程可能立刻获取到互斥量,并满足条件发送信号,然而此时还未执行pthread_cond_wait函数,因此等待的线程可能会错过这个信号,从而导致永远等待下去,这就是典型的竞态条件问题。

了解一下吧,我也不太明白......

相关推荐
醇氧19 分钟前
ab (Apache Bench)的使用
linux·学习·centos·apache
moneyxjj1 小时前
Linux各种解压命令汇总
linux·运维·服务器
白白♛~1 小时前
网络管理之---3种网络模式配置
linux·服务器·网络
GOTXX1 小时前
NAT、代理服务与内网穿透技术全解析
linux·网络·人工智能·计算机网络·智能路由器
脱了格子衬衫2 小时前
使用源码编译安装 Tomcat
linux·tomcat
陈yanyu2 小时前
Linux - 弯路系列3:安装和编译libvirt-4.5.0及虚拟网卡virbr0(virbr0-nic)创建
linux·运维·服务器
feng68_2 小时前
Linux编辑/etc/fstab文件不当,不使用快照;进入救援模式
linux
码农研究僧2 小时前
详细分析ip addr show 查看网络配置的命令
linux·tcp/ip·ip addr show
码狂☆2 小时前
ubuntu连接orangepi-zero-2w桌面的几种方法
linux·ubuntu
学习向前冲3 小时前
安装一键式重置密码插件(Linux)-CloudResetPwdAgent
linux·运维·服务器