【Linux】线程同步与互斥

news2024/11/25 2:37:10

文章目录

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

  • 加锁:

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

在这里插入图片描述
初始化互斥量有两种方法:

方法一:静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法二:动态分配

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调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

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

#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挂起的)。
  • 这就导致了死锁的问题。
#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 一把锁出现死锁的情况

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

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 代码演示:

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

#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内部已经帮我们想到了这一点,并做了相应的措施:(重点)

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 条件变量经典错误

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1040688.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

丢失d3dcompiler 47.dll的修复方案,哪个更值得推荐

d3dcompiler 47.dll 是 DirectX 中的一部分&#xff0c;它负责实现硬件加速的图形渲染。当我们运行一些需要 DirectX 支持的游戏或程序时&#xff0c;系统会调用 d3dcompiler 47.dll 文件。如果该文件丢失或损坏&#xff0c;我们将无法正常运行这些游戏或程序&#xff0c;从而给…

外汇天眼:交易的本质就是要解决这两个问题!

方向 在交易中&#xff0c;方向的判断至关重要。尽管判断对错在很大程度上是一个概率游戏&#xff0c;但我们可以细分方法来更好地解决这个问题。解决方向的方法可以总结为三大类&#xff1a; 1.通过容错和次数来解决方向 纯粹的逆市加码被认为是低级的做法&#xff0c;因为…

详细解析下gRPC examples-RBAC authenication-权限组管理-基于自定义Token

详细解析下gRPC examples-RBAC authenication-权限组管理-基于自定义Token 什么是RABC认证&#xff1f; RBAC (Role-Based Access Control) 授权策略是一种用于控制系统或应用程序中用户或实体对资源的访问权限的方法。在 RBAC 中&#xff0c;访问控制是基于角色的&#xff0…

快速找到离群值的三种方法

本文将介绍3个在数据集中查找离群值的Python方法 离群值&#xff08;Outliers&#xff09;是指在数据集中与其他数据点明显不同或者异常的数据点。这些数据点可能比其他数据点要远离数据集的中心&#xff0c;或者具有异常的数值。离群值可能是由于数据采集错误、异常事件、测量…

大型监控网络设备架构

IT监控架构的功效日益突出&#xff0c;已成为企业信息化建设不可或缺的一部分。本文将详细介绍IT监控架构的含义、构成、功能及其在公司中的应用。 IT监控架构的含义是什么&#xff1f; 简单来说&#xff0c;IT监控架构就是利用一系列技术和方法对公司的IT系统进行全方位的监控…

【LeetCode热题100】--73.矩阵置零

73.矩阵置零 使用标记数组&#xff1a; 使用两个标记数组分别记录每一行和每一列是否有零出现 先遍历一次数组&#xff0c;如果某个元素为0&#xff0c;则将该元素所在的行和列所对应的标记数组的位置为true&#xff0c;最后再遍历该数组&#xff0c;用标记数组更新原数即可 …

《优化接口设计的思路》系列:第四篇—接口的权限控制

系列文章导航 《优化接口设计的思路》系列&#xff1a;第一篇—接口参数的一些弯弯绕绕 《优化接口设计的思路》系列&#xff1a;第二篇—接口用户上下文的设计与实现 《优化接口设计的思路》系列&#xff1a;第三篇—留下用户调用接口的痕迹 《优化接口设计的思路》系列&#…

开学选什么样的电容笔好用?ipad可以用的手写笔

自从ipad等平板电脑开始使用电容笔以来&#xff0c;电容笔已经完全代替了我们的手指&#xff0c;并且使我们的书写速度有了很大的提高。但由于Apple Pencil内置的高科技芯片&#xff0c;价格始终居高不下&#xff0c;这让不少人&#xff0c;尤其是在校学生&#xff0c;也是难以…

DataGrip连接MySQL

DataGrip连接MySQL 新建项目 驱动管理 下载驱动 自定义驱动 如果网络环境不好 无法下载驱动 移除下载方式 指定自定义路径下的驱动 设置连接

linux驱动之input子系统简述

文章目录 一、什么是input子系统二、内核代码三、代码分析 一、什么是input子系统 Input驱动程序是linux输入设备的驱动程序&#xff0c;我们最常见的就按键&#xff0c;触摸&#xff0c;插拔耳机这些。其中事件设备驱动程序是目前通用的驱动程序&#xff0c;可支持键盘、鼠标…

资产连接支持会话分屏,新增Passkey用户认证方式,支持查看在线用户信息,JumpServer堡垒机v3.7.0发布

2023年9月25日&#xff0c;JumpServer开源堡垒机正式发布v3.7.0版本。在这一版本中&#xff0c;在用户管理层面&#xff0c;为了提高使用JumpServer操作资产的效率&#xff0c;JumpServer支持对会话进行分屏操作&#xff0c;用户可以在一个浏览器页面打开多个会话&#xff0c;方…

软件定义网络-OpenvSwitch

软件定义网络&#xff08;SDN&#xff09;。它主要有以下三个特点&#xff1a; 控制与转发分离&#xff1a;转发平面就是一个个虚拟或者物理的网络设备&#xff0c;就像小区里面的一条条路。控制平面就是统一的控制中心&#xff0c;就像小区物业的监控室。它们原来是一起的&…

[python 刷题] 853 Car Fleet

[python 刷题] 853 Car Fleet 哎……周赛第三题解应该用 monotonic stack 做优化的&#xff0c;没写出来&#xff0c;所以多刷两题 monotonic stack 的题目找找感觉…… 题目&#xff1a; There are n cars going to the same destination along a one-lane road. The destin…

【操作系统笔记九】并发安全问题

用户态抢占和内核态抢占 内核中可以执行以下几种程序&#xff1a; ① 当前运行的进程&#xff1a;陷阱程序&#xff08;系统调用&#xff09; 和 故障程序&#xff08;page fault&#xff09; &#xff0c;进程运行在内核态的时候&#xff0c;其实就是在执行进程在用户态触发的…

如何扫描MSI安装文件的路径

今天有个需求&#xff0c;需要扫描已经安装应用, 其中有个华云桌面 其中的UninstallString 值是 MsiExec.exe /X{D20A661B-0CBA-4DE3-A1F6-353D8153725D} 无法直接获取其安装目录&#xff0c; MsiGetProductInfoW 等API INSTALLPROPERTY_INSTALLLOCATION 也不好使 自己写一个…

Supervisor进程管理

Supervisor进程管理 概述&#xff1a;supervisor 是一个用 python 语言编写的进程管理工具&#xff0c;它可以很方便的监听、启动、停止、重启一个或多个进程。当一个进程意外被杀死&#xff0c;supervisor 监听到进程死后&#xff0c;可以很方便的让进程自动恢复&#xff0c;…

区块链实验室(26) - 区块链期刊Blockchain: Research and Applications

Elsevier出版物“Blockchain: Research and Applications”是浙江大学编审的期刊。该期刊自2020年创刊&#xff0c;并出版第1卷。每年出版4期&#xff0c;最新期是第4卷第3期(2023年9月)。 目前没有官方的IF&#xff0c;Elsevier的引用因子Citescore是6.4。 虽然是新刊&#xf…

《开发实战》18 | 数据存储:NoSQL与RDBMS如何取长补短、相辅相成?

取长补短之 Redis vs MySQL 做一个简单测试&#xff0c;分别填充 10 万条数据到 Redis 和 MySQL 中。MySQL 中的 name字段做了索引&#xff0c;相当于 Redis 的 Key&#xff0c;data 字段为 100 字节的数据&#xff0c;相当于 Redis 的Value。在我的电脑上&#xff0c;使用 wr…

免费的视频剪辑素材,可商用。

找免费可商用的视频剪辑素材&#xff0c;就上这6个网站&#xff0c;强推&#xff0c;赶紧收藏&#xff01; 1、菜鸟图库 https://www.sucai999.com/video.html?vNTYxMjky 菜鸟图库网素材非常丰富&#xff0c;网站主要还是以设计类素材为主&#xff0c;高清视频素材也很多&am…

【软件测试】黑盒测试用例的四种设计方法

一、输入域测试用例设计方法 输入域测试法是一种综合考虑了等价类划分、边界值分析等方法的综合方法&#xff0c;针对输入域测试法中可能出现的各种情况&#xff0c;输入域测试法主要考虑三个方面&#xff1a;  (1)极端测试(ExtremalTesting)&#xff0c;要求在输入域中选择…