【Linux多线程】线程的互斥与同步

发布时间:2024年01月18日

目录

Linux线程互斥

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

2. 互斥量mutex

3. 互斥量的接口?

3.1 初始化互斥量

3.2 销毁互斥量

3.3 互斥量加锁和解锁

4. 互斥量实现原理探究?

可重入VS线程安全

1. 概念

2. 常见的线程不安全的情况

3. 常见的线程安全的情况

4. 常见不可重入的情况

5. 常见可重入的情况?

6. 可重入与线程安全联系

7. 可重入与线程安全区别

常见锁概念?

1. 死锁

2. 死锁四个必要条件?

3. 避免死锁

Linux线程同步

1.?条件变量

2. 同步概念与竞态条件?

3. 条件变量

3.1 条件变量函数

3.2 条件变量使用规范


Linux线程互斥

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

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

2. 互斥量mutex

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

我们来看下面这个场景:

假如现在有1000张票,我们创建了4个线程来抢票,下面我们来看一下代码和运行结果:

#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>

using namespace std;

#define NUM 4

class threadData
{
public:
    threadData(int number)
    {
        threadName = "thread-" + to_string(number);
    }
public:
    string threadName;
};

int tickets = 1000;// 用多线程,模拟一轮抢票

void* getTickets(void* args)
{
    threadData* td = static_cast<threadData*>(args);
    const char* name = td->threadName.c_str();

    while (true)
    {
        if(tickets > 0)
        {
            usleep(1000);
            printf("who=%s, get a ticket: %d\n", name, tickets); // ?
            tickets--;
        }
        else
        {
            break;
        }
    }
    
    printf("%s ... quit\n", name);
    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    vector<threadData*> thread_datas;

    for (int i = 1; i < NUM; i++)
    {
        pthread_t tid;
        threadData* td = new threadData(i);

        thread_datas.push_back(td);
        pthread_create(&tid,nullptr,getTickets,thread_datas[i-1]);
        tids.push_back(tid);
    }
    
    for(auto thread:tids)
    {
        pthread_join(thread,nullptr);
    }

    for (auto td : thread_datas)
    {
        delete td;
    }

    return 0;
}

运行结果:

我们发现这里只有总共1000张票,getTickets函数中循环我们判断的是tickets > 0才进行抢票,按理说抢到1,应该就停止抢票了,但是现在票变成了负数,线程却还在抢。 这种情况会导致在同一张票卖出去了几次,在我们实际购票的时候是绝对不允许这种情况出现的。

那么为什么可能无法获得正确结果?原因有以下几点:

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

为什么说tickets-- 操作不是一个原子操作呢?我们取出ticket--部分的汇编代码:

objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

我们看到操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中
  • update : 更新寄存器里面的值,执行-1操作
  • store :将新值,从寄存器写回共享变量ticket的内存地址?

因为我们的操作需要三个步骤才能完成,那么就有可能出现这种情况:

  • 当一个线程正在加载tickets的值到内存中,然后它被切换出去。由于线程切走时保存了上下文信息,当第二个线程进入并执行操作时,它看到的tickets值仍然是1000。第二个线程抢了5张票后,tickets值变为995。
  • 随后,第一个线程再次被CPU调度,由于它保存了之前的上下文信息,它仍然认为还有1000张票。然后,它抢了2张票,并把新的tickets值998写回到内存。
  • 然而,由于第二个线程已经将tickets值从995修改为998,这就导致了票卖出次数的问题。原本应该剩余995张票,但因为两个线程的并发操作,现在票的数量从995变为了998,可能导致一张票被卖出多次。
  • 因此我们就可以知道对一个变量进行 ++ 或者 -- 操作它不是原子的。

这个问题称为竞态条件,它是由于多个线程对共享资源的并发访问和修改所引起的。为了解决这个问题,我们就需要采取一些办法来确保在任何时候只有一个线程可以访问和修改共享资源。

注意:

  • 寄存器不等于寄存器的内容
  • 线程在执行的时候,将共享数据加载到CPU寄存器的本质:把数据的内容,变成了自己的上下文 --- 以拷贝的方式,给自己单独拿了一份?

那么如何解决上面的问题呢?要解决以上问题,需要做到三点:

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

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

3. 互斥量的接口?

3.1 初始化互斥量

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

  • 方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • 方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);

参数:

  • mutex:要初始化的互斥量
  • attr:NULL

返回值:

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

3.2 销毁互斥量

销毁互斥量需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

3.3 互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回错误号

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

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

下面我们使用这些互斥量的这些接口来改进上面的售票系统:?

#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>

using namespace std;

#define NUM 4
int tickets = 1000;
//定义一把互斥锁
pthread_mutex_t lock;

class threadData
{
public:
    threadData(int number)
    {
        threadName = "thread-" + to_string(number);
    }
public:
    string threadName;
};

void* getTickets(void* args)
{
    threadData* td = static_cast<threadData*>(args);
    const char* name = td->threadName.c_str();

    while (true)
    {        
        pthread_mutex_lock(&lock);// 申请锁成功,才能往后执行,不成功,阻塞等待。
        if(tickets > 0)
        {
            usleep(1000);
            printf("who=%s, get a ticket: %d\n", name, tickets); // ?
            tickets--;
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
        usleep(13); // 防止同一个线程抢到很多票。我们抢到了票,我们会立马抢下一张吗?其实多线程还要执行得到票之后的后续动作。usleep模拟
    }

    printf("%s ... quit\n", name);
    return nullptr;
}

int main()
{
    pthread_mutex_init(&lock, NULL);

    vector<pthread_t> tids;
    vector<threadData*> thread_datas;

    for (int i = 1; i < NUM; i++)
    {
        pthread_t tid;
        threadData* td = new threadData(i);

        thread_datas.push_back(td);
        pthread_create(&tid,nullptr,getTickets,thread_datas[i-1]);
        tids.push_back(tid);
    }
    
    for(auto thread:tids)
    {
        pthread_join(thread,nullptr);
    }

    for (auto td : thread_datas)
    {
        delete td;
    }

    pthread_mutex_destroy(&lock);

    return 0;
}

运行结果:

可以看到使用了互斥量之后,这里的抢票抢到1就不会再抢了,达到了我们的预期效果。 我们发现代码中每次释放锁之后,我们都要进行sleep,这是为了防止同一个线程抢到很多票。我们抢到一张票并不会马上抢下一张票,而是会执行其他的动作。

下面我们用一个故事来加深对上面例子中每次抢完票为什么要sleep???????的理解:

假如现在有一个vip自习室(锁),里面只有一个座位,先到先得,你拿着门上的钥匙进去学习了。由于只有一个座位,后面的人(其它线程)只能排队等着。当你学习完,把钥匙挂回门上。挂回去之后你怕自习室被人占了,又马上把钥匙拿下来,由于这时候你离门最近,其它人竞争不过你,所以你又申请到了自习室。但是你占着自习室不干活,其他人又等着自习室。这就造成了线程饥饿问题。

为了解决这个问题。自习室观察员提出来两条规则:1.外面来的人,必须排队 2.出来的人,不能立马申请锁,必须排队到队列的尾部。上面代码中我们释放锁完用一个sleep来模拟出来的线程,不能立马申请锁,必须排队到队列的尾部。?

注意:我们判断tickets是否大于0的时候,判断是访问临界资源吗?必须是的,也就是判断tickets是否大于0必须在加锁之后!!! (你怎么知道临界资源是就绪还是不就绪的?你判断出来的!判断是访问临界资源吗?必须是的,也就是判断必须在加锁之后!!! )?

我们的线程在访问临界区的代码之前都必须要先申请锁,那也就意味着所有的线程必须看到同一把锁,那锁是不是临界资源呢?

  • 锁本身就是临界资源,我们通过锁去保护临界区, 那我们的锁需不需要保护自身的安全呢?答案是当然需要的(保护不了自己如何保护他人)
  • 因此我们需要保证我们的锁本身是原子性的,不能够出现中间态。

注意:

加锁的本质:用时间来换取安全!

  • 我们加锁之后,代码的执行效率一般会降低,这是因为在大部分情况下,加锁本身都是有损于性能的事,它会让多执行流由并行执行变为了串行执行,这几乎是不可避免的。

加锁的表现:线程对于临界区代码串行执行

加锁原则:尽量的要保证临界区代码,越少越好!

代码是程序员写的!为了保证临界区的安全,必须保证每个线程都遵守相同的编码规范(A线程申请锁,其他线程的代码也必须申请锁)

4. 互斥量实现原理探究?

  • 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

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

我们假设这里的mutex的初始值为1,然后al是一个寄存器,当线程去申请锁时,需要执行以下几步:

  1. 线程将al寄存器的值置为0,然后再将al寄存器里面的值与mutex值进行交换
  2. 交换完成之后,对al寄存器里面的值进行判断,如果值大于0,则申请锁成功,如果al寄存器里面的值小于0则需要挂起等待
  3. 申请到锁的线程快执行完自己的任务时,释放刚刚申请到的锁,然后刚刚由于竞争锁失败的线程会被唤醒重新去竞争锁资源。

下面我们来展示一下多个线程申请锁的具体过程:

前面我们说过,我们要注意:寄存器内容属于线程的上下文,每个线程都有自己的寄存器内容,当线程被调度时,会把自己的内容加载到寄存器。线程被切换时会把自己的上下文带走! !

现在有两个线程,线程1、线程2,线程1与线程2刚开始都将al寄存器的值置为0

线程1先将a1寄存器里面的值与mutex值交换,线程2再将a1寄存器里面的值与mutex值进行交换,因为1先将寄存器里面的值与mutex值交换了,所以等2进行交换的时候内存里面mutex里面的值已经变成0了,因此2交换后a1寄存器里面的值和内存里面mutex的值都是0。

假如此时线程1的时间片到了,然后线程1会被切走,但是线程1还没有释放锁(所以被切走的时候线程1的上下文信息会被保存起来)。这时候线程2去申请锁,因为此时线程2它al寄存器里面的值是0,因此它申请不到锁所以他要挂起等待。

CPU重新开始调度线程1继续执行,此时线程1申请锁,因为此时线程1它al寄存器上下文的值是大于0的,因此它能够申请到锁。

当线程A快执行完自己的任务时,就会释放刚刚申请到的锁,此时内存中的mutex值会被置成1,刚刚由于竞争锁失败而挂起等待的线程B此时会被唤醒然后去重新竞争锁。

以上就是互斥量的实现原理了。这里要注意:为什么当一个线程它申请到锁之后,即使它被切走了,但是其他的线程还是申请不到锁,这是因为即使它被切走了但是因为它的寄存器里面的值为1。所以也就相当于它是拿着钥匙走的,别人是拿不到这把锁的。

可重入VS线程安全

1. 概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

2. 常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

3. 常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性?

4. 常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构?

5. 常见可重入的情况?

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

6. 可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。?

7. 可重入与线程安全区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。?

常见锁概念?

1. 死锁

  • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。?

2. 死锁四个必要条件?

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

下面我来给大家讲一个小故事帮助大家记忆:

张三和李四都各自有20块钱,但是买麦当劳套餐却要40块钱,这个时候张三对李四说:李四啊,你把你手上的20钱给我吧,我去买一包烟。李四对张三说:张三啊,你把你手上的20块钱给我吧,我去买肯德基套餐吃。这个时候张三向李四要她20块钱,李四向张三要她的20块钱,但是两个人都不肯把自己的20块钱给对方。因为法治社会所以不会出现直接抢夺对方钱的情况,因此这就形成了死锁。

上面的这个小故事就体现了死锁的四个必要条件:

张三和李四各自有20块钱(互斥条件),两个人都向对方要对方的20块钱,但是不肯将自己的20块钱给对方(请求与保持条件),因为两个人是好朋友所以不会出现直接抢夺对方20块钱的情况(不剥夺条件),于是两个人就形成一种头尾相接的循环等待资源的关系(循环等待条件)

注意:以上四个必要条件必须同时满足!?

我们来看一段死锁的代码:

#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;//线程1的锁
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;//线程2的锁

void* threadRoutine1(void* arg)
{
    const char* name1 = (char*)arg;
    //加锁
    pthread_mutex_lock(&lock1);
    while(true)
    {
        cout<<"threadname: "<<name1<<endl;
        sleep(1);
        //申请线程2的锁
        pthread_mutex_lock(&lock2);
    }
    //解锁
    pthread_mutex_unlock(&lock1);
}

void* threadRoutine2(void* arg)
{
    const char* name2 = (char*)arg;
    //加锁
    pthread_mutex_lock(&lock2);
    while(true)
    {
        cout<<"threadname: "<<name2<<endl;
        sleep(1);
        //申请线程1的锁
        pthread_mutex_lock(&lock1);
    }
    //解锁
    pthread_mutex_unlock(&lock2);
}

int main()
{
    pthread_t tid1,tid2;
    pthread_mutex_init(&lock1,nullptr);
    pthread_mutex_init(&lock2,nullptr);
    
    pthread_create(&tid1,nullptr,threadRoutine1,(void*)"thread 1");
    pthread_create(&tid2,nullptr,threadRoutine2,(void*)"thread 2");
    
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    
    return 0;
}

?运行结果:?

?

3. 避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

避免死锁算法:死锁检测算法(了解)、银行家算法(了解)

Linux线程同步

1.?条件变量

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

2. 同步概念与竞态条件?

  • 同步:保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。

前面抢票的例子里面,我们说过为了避免同一个线程抢到很多票,我们使用usleep函数进行模拟其它动作,因为实际上抢完票我们不可能马上抢下一张,还需要做一些其他的事情(绑定信息等)。同样的,我们只进行单纯的加锁,如果一个线程竞争力很强,每次都是申请到锁,但是不干活有可能会导致其它线程长时间竞争不到锁,从而引起饥饿问题。

单纯的加锁可以保证在同一时间只有一个线程进入临界区访问临界资源,但是它没有高效的让每一个线程使用这份临界资源。

3. 条件变量

概念:条件变量是用于多线程编程的一种同步机制,它允许线程等待某个特定条件成立或满足某些条件后再执行。它是用来描述某种临界资源是否就绪的一种数据化描述。

条件变量常常与互斥锁(mutex)一起使用。

3.1 条件变量函数

初始化条件变量函数:

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);

参数:

  • cond:要初始化的条件变量
  • attr:NULL

返回值:

  • 初始化成功返回0,失败返回错误码

销毁条件变量函数:

int pthread_cond_destroy(pthread_cond_t *cond)

参数说明:

  • cond: 要销毁的条件变量

返回值:

  • 条件变量销毁成功返回0,失败返回错误码

等待条件满足:

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

参数:

  • cond:要在这个条件变量上等待
  • mutex:互斥量,后面详细解释?

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

参数说明:

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

区别:

  • pthread_cond_signal函数用于唤醒等待队列中首个线程。
  • pthread_cond_broadcast函数用于唤醒等待队列中的全部线程。

返回值:

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

了解了条件变量的函数之后,下面我们来看一段线程同步的简单案例:

#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

int cnt = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void* Count(void*args)
{
    pthread_detach(pthread_self());
    uint64_t num = (uint64_t)args;//这里用int强转的话是4个字节,而64位下指针是8个字节,所以我们用8字节的无符号长整型的uint64_t来强转和接收
    cout << "pthread:" << num << " create sucess" << endl;

    while (true)
    {
        pthread_mutex_lock(&mutex);
        // 我们怎么知道我们要让一个线程去休眠了那?一定是临界资源不就绪,没错,临界资源也是有状态的!!
        // 你怎么知道临界资源是就绪还是不就绪的?你判断出来的!判断是访问临界资源吗?必须是的,也就是判断必须在加锁之后!!!
        //每个线程进来都到等待队列里面去等待唤醒
        pthread_cond_wait(&cond,&mutex); //? 为什么在这里? 1. pthread_cond_wait让线程等待的时候,会自动释放锁!
        // 不管临界资源的状态情况
        cout << "pthread:" << num << " ,cnt:" << cnt++ << endl;
        pthread_mutex_unlock(&mutex);
    }
    
}

int main()
{
    for (int i = 0; i < 5; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, Count, (void*)i);//i不能取地址,如果取地址新线程和主线程用的就是同一个i,如果主线程i++的动作比新线程快执行,那么新线程取到的i就变了
        usleep(1000);
    }
    sleep(3);
    cout << "main thread ctrl begin: " << endl;
    
    while (true)
    {
        sleep(1);
        pthread_cond_signal(&cond);//唤醒在cond的等待队列中等待的一个线程,默认都是第一个
        std::cout << "signal one thread..." << std::endl;
    }
    

    return 0;
}

运行结果:

通过运行结果我们可以看到,我们唤醒的这五个线程是有顺序性的,主要是因为这几个线程启动时默认都会在该条件变量下去等待,而我们通过pthread_cond_signal函数每次都唤醒的是在当前条件变量下等待的首个线程,当该线程执行完打印操作后会继续排到等待队列的尾部进行wait,所以我们能够唤醒的线程是有顺序性的。

我们也可以通过pthread_cond_broadcast函数每次唤醒在该条件变量下等待的所有线程:

运行结果:

可以看到我们这一次的唤醒是一次唤醒了所有在cond条件变量下等待的线程。?

我们来看下面这幅图加深对上面这个例子的理解:
为什么phread_cond_wait需要互斥量?

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

  • 当某个线程进入临界区访问临界资源时需要先加锁,然后判断内部资源的情况,如果不满足当前线程的执行条件,那么当前线程需要在该条件变量下进行等待,但此时该线程是拿着锁被挂起的,这也就意味着这个锁再也不会被释放了,此时就会发生死锁问题。
  • 所以在调用pthread_cond_wait函数时,还需要将对应的互斥锁传入,此时当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁。
  • 当该线程被唤醒时,该线程会接着执行临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁,因此当某一个线程被唤醒时,实际会自动获得对应的互斥锁。

3.2 条件变量使用规范

  • 等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);

为什么pthread_cond_wait要在lock和unlock之间?

  1. 我们怎么知道我们要让一个线程去休眠了呢?一定是临界资源不就绪,没错,临界资源也是有状态的!!
  2. 你怎么知道临界资源是就绪还是不就绪的?你判断出来的!判断是访问临界资源吗?必须是的,也就是判断必须在加锁之后!!!

pthread_cond_wait必须在lock和unlock之间,因为它是用来处理线程间的同步的。具体来说,pthread_cond_wait的作用是根据某个条件来等待,当条件满足时,线程会被唤醒并继续执行。而这个条件通常与某个共享变量的状态有关。为了确保线程安全地访问共享变量,需要使用互斥锁(mutex)来保护共享变量。

在调用pthread_cond_wait之前,需要先对互斥锁进行加锁操作,以防止其他线程同时修改共享变量。在调用pthread_cond_wait之后,需要再对互斥锁进行解锁操作,以便其他等待该互斥锁的线程可以获得执行机会。

如果不在lock和unlock之间使用pthread_cond_wait,那么可能会出现竞态条件(race condition),即多个线程同时访问和修改共享变量,导致数据不一致或不可预期的行为。使用互斥锁可以避免这种情况,确保只有一个线程可以访问共享变量,从而保证数据的一致性和正确性。

因此,pthread_cond_wait必须在lock和unlock之间使用,以确保线程安全地等待某个条件成立或满足某些条件。

  • 给条件发送信号代码?
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

注意:

  • 条件变量通常需要配合mutex互斥锁一起使用,其中条件变量是用来完成同步的,而互斥锁是用来完成互斥的。
  • pthread_cond_wait函数有两个功能,一是让线程在特定的条件变量下进行等待,二是让线程释放掉自己申请到的互斥锁。当该线程被唤醒后,该线程会立马获得之前释放的互斥锁,然后继续向下执行。
文章来源:https://blog.csdn.net/gtyyky/article/details/135156409
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。