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

目录

二十.线程互斥

        20.1 什么是线程互斥?

        20.2 为什么需要线程互斥?

        20.3 互斥锁mutex

        20.4 互斥量的接口

        20.4.1 互斥量初始

        20.4.2 互斥量销毁

        20.4.3 互斥量加锁

        20.4.4 互斥量解锁

        20.4.5 互斥量的基本原理

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

        20.5 死锁问题

        死锁的四个必要条件

        如何避免死锁

        20.6 互斥量的实现机制

二十一.线程同步

        21.1 同步概念与竞态条件

        21.2 条件变量

        21.2.1 条件变量初始

        21.2.2 条件变量销毁

        21.2.3 等待满足

        21.2.3 唤醒等待

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

        21.4 为什么pthread_cond_wait需要互斥量?

        21.5 条件变量使用规范


二十.线程互斥

        20.1 什么是线程互斥?

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

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

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

        20.2 为什么需要线程互斥?

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

#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 函数进行初始化,其原型如下:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

参数说明:

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

返回值说明:

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

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

20.4.2 互斥量销毁

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

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数说明:

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

返回值说明:

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

销毁互斥量需要注意:

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

20.4.3 互斥量加锁

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

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数说明:

  • mutex:要加锁的互斥量

返回值说明:

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

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

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

20.4.4 互斥量解锁

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

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 带上互斥锁后的抢票程序

#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 死锁问题

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

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

#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 条件变量初始

初始化分为两种:

//动态分配
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 函数,用于释放条件变量占用的资源。

int pthread_cond_destroy(pthread_cond_t *cond);

参数说明:

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

返回值说明:

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

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

21.2.3 等待满足

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

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

参数说明:

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

返回值说明:

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

21.2.3 唤醒等待

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

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

int pthread_cond_signal(pthread_cond_t *cond);

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

int pthread_cond_broadcast(pthread_cond_t *cond);

参数说明:

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

返回值说明:

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

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

#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函数时,还需要将对应的互斥锁传入,此时当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁。
  • 当该线程被唤醒时,该线程会接着执行临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁,因此当某一个线程被唤醒时,实际会自动获得对应的互斥锁。

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

//错误的设计
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 条件变量使用规范

等待条件变量的代码

pthread_mutex_lock(&mutex);
while (条件为假)
	pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);

唤醒等待线程的代码

pthread_mutex_lock(&mutex);
//设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【医学大模型 动态知识图谱】AliCG概念图 = 知识图谱 + 实时更新、细粒度概念挖掘、个性化适应

AliCG概念图 提出背景能力强化细粒度概念获取长尾概念挖掘分类体系进化对比传统知识图谱 部署方法如何提高信息检索的质量&#xff1f;如何在神经网络中学习概念嵌入&#xff1f;如何在预训练阶段利用概念图&#xff1f; 提出背景 论文: https://arxiv.org/pdf/2106.01686.pdf…

论文解读:MobileOne: An Improved One millisecond Mobile Backbone

论文创新点汇总&#xff1a;人工智能论文通用创新点(持续更新中...)-CSDN博客 论文总结 关于如何提升模型速度&#xff0c;当今学术界的研究往往聚焦于如何将FLOPs或者参数量的降低&#xff0c;而作者认为应该是减少分支数和选择高效的网络结构。 概述 MobileOne(≈MobileN…

《剑指Offer》笔记题解思路技巧优化 Java版本——新版leetcode_Part_2

《剑指Offer》笔记&题解&思路&技巧&优化_Part_2 &#x1f60d;&#x1f60d;&#x1f60d; 相知&#x1f64c;&#x1f64c;&#x1f64c; 相识&#x1f353;&#x1f353;&#x1f353;广度优先搜索BFS&#x1f353;&#x1f353;&#x1f353;深度优先搜索DF…

九、java 继承

文章目录 java 继承3.1 根父类Object3.2 方法重写3.3 继承案例&#xff1a;图形类继承体系3.4 继承的细节3.4.1 构造方法3.4.2 重名与静态绑定3.4.3 重载和重写3.4.4 父子类型转换3.4.5 继承访问权限protected3.4.6 可见性重写3.4.7 防止继承final 3.5 继承是把双刃剑3.5.1 继承…

70.SpringMVC怎么和AJAX相互调用的?

70.SpringMVC怎么和AJAX相互调用的&#xff1f; &#xff08;1&#xff09;加入Jackson.jar&#xff08;2&#xff09;在配置文件中配置json的消息转换器.(jackson不需要该配置HttpMessageConverter&#xff09; <!‐‐它就帮我们配置了默认json映射‐‐> <mvc:anno…

Netty应用——实例-群聊系统(十六)

编写一个Netty群聊系统&#xff0c;实现服务器端和客户端之间的数据简单通讯 (非阻塞)实现多人群聊服务器端:可以监测用户上线&#xff0c;离线&#xff0c;并实现消息转发功能客户端:通过channel可以无阳塞发送消息给其它所有用户&#xff0c;同时可以接受其它用户发送的消息(…

哈夫曼树的学习以及实践

哈夫曼树 哈夫曼树的基本了解哈夫曼树的基本概念创建霍夫曼树的思路编码构建的思路代码实现创建HuffmanTree结点初始化HuffmanTree创建霍夫曼树霍夫曼树编码 哈夫曼树的基本了解 给定 n 个 权值 作为 n 个 叶子节点&#xff0c;构造一颗二叉树&#xff0c;若该树的 带权路径长…

C语言第二十三弹---指针(七)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 指针 1、sizeof和strlen的对比 1.1、sizeof 1.2、strlen 1.3、sizeof 和 strlen的对比 2、数组和指针笔试题解析 2.1、⼀维数组 2.2、二维数组 总结 1、si…

C语言每日一题(56)平衡二叉树

力扣网 110 平衡二叉树 题目描述 给定一个二叉树&#xff0c;判断它是否是高度平衡的二叉树。 本题中&#xff0c;一棵高度平衡二叉树定义为&#xff1a; 一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,…

牛客错题整理——C语言(实时更新)

1.以下程序的运行结果是&#xff08;&#xff09; #include <stdio.h> int main() { int sum, pad,pAd; sum pad 5; pAd sum, pAd, pad; printf("%d\n",pAd); }答案为7 由于赋值运算符的优先级高于逗号表达式&#xff0c;因此pAd sum, pAd, pad;等价于(…

Linux系统之部署File Browser文件管理系统

Linux系统之部署File Browser文件管理系统 一、File Browser介绍1.1 File Browser简介1.2 File Browser功能1.3 File Browser使用场景 二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍 三、检查本地环境3.1 检查本地操作系统版本3.2 检查系统内核版本 四、安装File Browser4…

Linux_线程

线程与进程 多级页表 线程控制 线程互斥 线程同步 生产者消费者模型 常见概念 下面选取32位系统举例。 一.线程与进程 上图是曾经我们认为进程所占用的资源的集合。 1.1 线程概念 线程是一个执行分支&#xff0c;执行粒度比进程细&#xff0c;调度成本比进程低线程是cpu…

SpringCloud-Eureka服务注册中心测试实践

5. Eureka服务注册中心 5.1 什么是Eureka Netflix在涉及Eureka时&#xff0c;遵循的就是API原则.Eureka是Netflix的有个子模块&#xff0c;也是核心模块之一。Eureka是基于REST的服务&#xff0c;用于定位服务&#xff0c;以实现云端中间件层服务发现和故障转移&#xff0c;服…

fast.ai 深度学习笔记(六)

深度学习 2&#xff1a;第 2 部分第 12 课 原文&#xff1a;medium.com/hiromi_suenaga/deep-learning-2-part-2-lesson-12-215dfbf04a94 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 来自 fast.ai 课程的个人笔记。随着我继续复习课程以“真正”理解它&#xff0c;…

Java 基于微信小程序的私家车位共享系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

LC 987. 二叉树的垂序遍历

987. 二叉树的垂序遍历 难度 : 困难 题目大意&#xff1a; 给你二叉树的根结点 root &#xff0c;请你设计算法计算二叉树的 垂序遍历 序列。 对位于 (row, col) 的每个结点而言&#xff0c;其左右子结点分别位于 (row 1, col - 1) 和 (row 1, col 1) 。树的根结点位于 …

爬虫2—用爬虫爬取壁纸(想爬多少张爬多少张)

先看效果图&#xff1a; 我这个是爬了三页的壁纸60张。 上代码了。 import requests import re import os from bs4 import BeautifulSoupcount0 img_path "./壁纸图片/"#指定保存地址 if not os.path.exists(img_path):os.mkdir(img_path) headers{ "User-Ag…

【STL】string的模拟实现

string类的模拟实现 一、接口函数总览二、默认成员函数1、构造函数2、拷贝构造函数&#xff08;1&#xff09;写法一&#xff1a;传统写法&#xff08;2&#xff09;写法二&#xff1a;现代写法 3、赋值运算符重载函数&#xff08;1&#xff09;写法一&#xff1a;传统写法&…

【开源】JAVA+Vue.js实现天然气工程运维系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统角色分类2.2 核心功能2.2.1 流程 12.2.2 流程 22.3 各角色功能2.3.1 系统管理员功能2.3.2 用户服务部功能2.3.3 分公司&#xff08;施工单位&#xff09;功能2.3.3.1 技术员角色功能2.3.3.2 材料员角色功能 2.3.4 安…

东风联手华为打造首款SUV,车长超5米,配纯电和增程双动力系统

在上个月&#xff08;2024年1月份&#xff09;&#xff0c;东风汽车和华为达成了战略合作计划&#xff0c;两家品牌将联手打造全新的汽车品牌——奕派汽车&#xff0c;而目前我们从相关渠道获悉&#xff0c;其首款SUV车型已经获得了实拍亮相&#xff0c;而新车的内部代号为S59&…