【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函数,因此等待的线程可能会错过这个信号,从而导致永远等待下去,这就是典型的竞态条件问题。

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

相关推荐
HLC++8 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室10 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy12 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕13 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫14 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生14 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1116 小时前
linux基础IO
linux·运维·服务器
BelongPanda17 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯18 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983418 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器