当前位置: 首页 > news >正文

Linux线程安全

线程安全

  • Linux线程互斥
    • 进程线程间的互斥相关背景概念
    • 互斥量mutex
    • 互斥量的接口
    • 互斥量实现原理探究
  • 可重入VS线程安全
    • 概念
    • 常见的线程不安全的情况
    • 常见的线程安全的情况
    • 常见的不可重入的情况
    • 常见的可重入的情况
    • 可重入与线程安全联系
    • 可重入与线程安全区别
  • 常见锁概念
    • 死锁
    • 死锁的四个必要条件
    • 避免死锁
  • Linux线程同步
    • 同步概念与竞态条件
    • 条件变量
    • 条件变量函数

Linux线程互斥

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

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

临界资源和临界区

进程之间如果要进行通信我们需要先创建第三方资源,让不同的进程看到同一份资源,由于这份第三方资源可以由操作系统中的不同模块提供,于是进程间通信的方式有很多种。进程间通信中的第三方资源就叫做临界资源,访问第三方资源的代码就叫做临界区。

而多线程的大部分资源都是共享的,线程之间进行通信不需要费那么大的劲去创建第三方资源。

例如,我们在全局区定义一个count变量,让新线程每隔一秒对该变量加一操作,让主线程每隔一秒获取count变量的值进行打印:

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>using namespace std;int count = 0;void* Routine(void* args)
{while(true){count++;sleep(1);}pthread_exit((void*)0);
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, Routine, nullptr);while(true){cout << "count: " <<count <<endl;sleep(1);}pthread_join(tid, nullptr);return 0;
}

运行代码:
在这里插入图片描述
此时我们相当于实现了主线程和新线程之间的通信,其中全局变量count就叫做临界资源,因为它被多个执行流共享,而主线程中的printf和新线程中count++就叫做临界区,因为这些代码对临界资源进行了访问。

互斥和原子性

在多线程情况下,如果这多个执行流都自顾自的对临界资源进行操作,那么此时就可能导致数据不一致的问题。解决该问题的方案就叫做互斥,互斥的作用就是,保证在任何时候有且只有一个执行流进入临界区对临界资源进行访问。

原子性指的是不可被分割的操作,该操作不会被任何调度机制打断,该操作只有两态,要么完成,要么未完成。

下面我们模拟实现一个抢票系统,我们将记录票的剩余张数的变量定义为全局变量,主线程创建三个新线程,让这三个新线程进行抢票,当票被抢完后这四个线程自动退出:

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>using namespace std;int tickets = 10000;void* getTickets(void* args)
{(void*) args;while(true){if(tickets > 0){usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;}else{break;}}pthread_exit((void*)0);
}
int main()
{pthread_t t1, t2, t3;pthread_create(&t1, nullptr, getTickets, nullptr);pthread_create(&t2, nullptr, getTickets, nullptr);pthread_create(&t3, nullptr, getTickets, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
}

运行代码,我们会发现,出现了票数为负的情况,显然这是不正常的:
在这里插入图片描述
该代码中记录剩余票数的变量tickets就是临界资源,因为它被多个执行流同时访问,而判断tickets是否大于0、打印剩余票数以及tickets--这些代码就是临界区,这些代码对临界资源进行了访问。

剩余票数出现负数的原因:

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

为什么ticket--不是原子操作?

我们对一个变量进行--,我们实际需要进行以下三个步骤:

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

在这里插入图片描述
操作对应的汇编代码如下:
在这里插入图片描述
当thread 1 刚把tickets的值读进CPU时就可能已经被切走了,从CPU剥离下来,并没有进行--操作,假如我们此时thread 1的信息就是10000,当他被切换走以后,他的上下文信息就会被保存下来,此时thread 1就处于挂起状态。
在这里插入图片描述

此时,thread 2正好被调度了,而我们的thread 1 刚把tickets的值读进CPU时就被切换走了,还没有进行--操作,此时thread 2也是从10000开始进行--操作,假设OS分配给thread 2的时间比较长,此时thread 2进程1000次--操作并且被切回内存。
在这里插入图片描述
此时,thread 2切换走,thread 1恢复回来,操作系统会让thread 1上下文数据恢复,继续执行刚刚的操作,此时我们的tickets就又变为了10000,进行一次--操作以后变为9999,此时在切回内存。
在这里插入图片描述
在上述过程中,thread1抢了1张票,thread2抢了1000张票,而此时剩余的票数却是9999,也就相当于多出了1000张票。

因此对一个变量进行--操作并不是原子的,虽然–tickets看起来就是一行代码,但这行代码被编译器编译后本质上是三行汇编,相反,对一个变量进行++也需要对应的三个步骤,即++操作也不是原子操作。

互斥量mutex

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

要解决上述抢票系统的问题,需要做到三点:

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

要做到这三点,本质上就是需要一把锁,Linux上提供的这把锁叫互斥量。
在这里插入图片描述

互斥量的接口

初始化互斥量

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

  • 方法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,失败返回错误码

销毁互斥量

销毁互斥量的函数叫做pthread_mutex_destroy,该函数的函数原型如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数说明:

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

返回值说明:

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

销毁互斥量需要注意:

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

互斥量加锁

互斥量加锁的函数叫做pthread_mutex_lock,该函数的函数原型如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要加锁的互斥量。

返回值说明:

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

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

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

互斥量解锁

互斥量解锁的函数叫做pthread_mutex_unlock,该函数的函数原型如下:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要解锁的互斥量。

返回值说明:

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

我们在上述的抢票系统中引入互斥量,每一个线程要进入临界区之前都必须先申请锁,只有申请到锁的线程才可以进入临界区对临界资源进行访问,并且当线程出临界区的时候需要释放锁,这样才能让其余要进入临界区的线程继续竞争锁。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <pthread.h>using namespace std;
#define THREAD_NUM 1000int tickets = 10000;class ThreadData
{
public:ThreadData(const string& name, pthread_mutex_t* mtx):tname(name),pmtx(mtx){}public:string tname;pthread_mutex_t* pmtx;
};void *getTickets(void *args)
{ThreadData* td = (ThreadData*)args;while (true){int n = pthread_mutex_lock(td->pmtx);assert(n==0);if (tickets > 0){usleep(1000);printf("%s: %d\n", td->tname.c_str(), tickets);tickets--;n = pthread_mutex_unlock(td->pmtx);assert(n==0);}else{n = pthread_mutex_unlock(td->pmtx);assert(n==0);break;}}delete td;pthread_exit((void *)0);
}
int main()
{pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);pthread_t t[THREAD_NUM];for(int i = 0; i < THREAD_NUM; i++){string name = "thread";name += to_string(i+1);ThreadData* td = new ThreadData(name, &mutex);pthread_create(t+i, nullptr, getTickets, (void*)td);}for(int i = 0; i < THREAD_NUM; i++){pthread_join(t[i], nullptr);}pthread_mutex_destroy(&mutex);return 0;
}

运行代码,此时在抢票过程中就不会出现票数剩余为负数的情况了。
在这里插入图片描述

  • 在大部分情况下,加锁本身都是有损于性能的事,它让多执行流由并行执行变为了串行执行,这几乎是不可避免的。
  • 我们应该在合适的位置进行加锁和解锁,这样能尽可能减少加锁带来的性能开销成本。
  • 进行临界资源的保护,是所有执行流都应该遵守的标准,这时程序员在编码时需要注意的。

互斥量实现原理探究

加锁后的原子性体现在哪里?

引入互斥量后,当一个线程申请到锁进入临界区时,在其他线程看来该线程只有两种状态,要么没有申请锁,要么锁已经释放了,因为只有这两种状态对其他线程才是有意义的。

例如,图中线程1进入临界区后,在线程2、3、4看来,线程1要么没有申请锁,要么线程1已经将锁释放了,因为只有这两种状态对线程2、3、4才是有意义的,当线程2、3、4检测到其他状态时也就被阻塞了。
在这里插入图片描述
此时对于线程2、3、4而言,它们就认为线程1的整个操作过程是原子的。

临界区内的线程可能进行线程切换吗?

临界区内的线程完全可能进行线程切换,但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。

其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。

锁是否需要被保护?

我们说被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。所有的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个执行流共享的资源,也就是说锁本身就是临界资源。

既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?

锁实际上是自己保护自己的,我们只需要保证申请锁的过程是原子的,那么锁就是安全的。

如何保证申请锁的过程是原子的?

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

下面我们来看看lock和unlock的伪代码:
在这里插入图片描述
我们可以认为mutex的初始值为1,al是计算机中的一个寄存器,当线程申请锁时,需要执行以下步骤:

  • 先将al寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(上下文信息),执行该动作本质上是将自己的al寄存器清0。
  • 然后交换al寄存器和mutex中的值。xchgb是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。
  • 最后判断al寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁。

接下来我们来详细解释一下:

  1. 假设我们有两个线程A,B,此时线程A需要进行操作,内存中mutex的值为1,线程申请锁时先将al寄存器中的值清0:
    在这里插入图片描述
  2. 当线程A完成第一步操作以后,此时操作系统给线程A分配的时间到了,线程A带着他的上下文数据就被切换走了,此时线程B进程操作,同样,先将al寄存器中的值清0:
    在这里插入图片描述
  3. 然后线程B将al寄存器中的值与内存中mutex的值进行交换。
    在这里插入图片描述
  4. 此时操作系统给线程B分配的时间刚好也到了,就又切换回线程A,线程A带着他的上下文数据就回来了,继续进行刚才第二步操作,交换数据,此时al寄存器中的值也就变成了0,但由于刚才mutex的值被线程B进行了交换,此时mutex也为0,两者交换以后,还是0:
    在这里插入图片描述

我们就会发现,线程A进入判断步骤以后,al寄存器中的值此时不大于0,就会挂起等待:
在这里插入图片描述

  • 此时,OS发现线程A在等待,就右切换回线程B,完成刚才未完成操作,return以后,进行解锁操作,将mutex置为1,在将等待的线程A唤醒:
    在这里插入图片描述
    如此,也就可以保证申请锁过程的原子性了。

注意

  • 在申请锁时本质上就是哪一个线程先执行了交换指令,那么该线程就申请锁成功,因为此时该线程的al寄存器中的值就是1了。而交换指令就只是一条汇编指令,一个线程要么执行了交换指令,要么没有执行交换指令,所以申请锁的过程是原子的。
  • 在线程释放锁时没有将当前线程al寄存器中的值清0,这不会造成影响,因为每次线程在申请锁时都会先将自己al寄存器中的值清0,再执行交换指令。
  • CPU内的寄存器不是被所有的线程共享的,每个线程都有自己的一组寄存器,但内存中的数据是各个线程共享的。申请锁实际就是,把内存中的mutex通过交换指令,原子性的交换到自己的al寄存器中。

可重入VS线程安全

概念

线程安全: 多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现线程安全问题。

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

注意: 线程安全讨论的是线程执行代码时是否安全,重入讨论的是函数被重入进入。

常见的线程不安全的情况

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

常见的线程安全的情况

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

常见的不可重入的情况

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

常见的可重入的情况

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

可重入与线程安全联系

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

可重入与线程安全区别

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

常见锁概念

死锁

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

如下图所示,线程A申请申请了锁1,并不会进行释放,线程B申请了锁2,也不会进行释放,此时线程A需要申请锁2,线程B需要申请锁1,显然是不可能成功的,这就叫做死锁。

在这里插入图片描述

单执行流可能产生死锁吗?

单执行流也有可能产生死锁,如果某一执行流连续申请了两次锁,那么此时该执行流就会被挂起。因为该执行流第一次申请锁的时候是申请成功的,但第二次申请锁时因为该锁已经被申请过了,于是申请失败导致被挂起直到该锁被释放时才会被唤醒,但是这个锁本来就在自己手上,自己现在处于被挂起的状态根本没有机会释放锁,所以该执行流将永远不会被唤醒,此时该执行流也就处于一种死锁的状态。

我们看下面这段代码:

#include <iostream>
#include <cstdio>
#include <pthread.h>using namespace std;pthread_mutex_t mutex;void *Routine(void *args)
{pthread_mutex_lock(&mutex);while(true){pthread_mutex_lock(&mutex);cout << "new thread runing....." << endl;}pthread_exit((void*)0);
}int main()
{pthread_t tid;pthread_mutex_init(&mutex, nullptr);pthread_create(&tid, nullptr, Routine, nullptr);pthread_join(tid, nullptr);pthread_mutex_destroy(&mutex);return 0;
}

运行代码,此时该程序实际就处于一种被挂起的状态:
在这里插入图片描述
用ps命令查看该进程时可以看到,该进程当前的状态是Sl+,其中的l实际上就是lock的意思,表示该进程当前处于一种死锁的状态。
在这里插入图片描述

什么叫做阻塞?

进程运行时是被CPU调度的,换句话说进程在调度时是需要用到CPU资源的,每个CPU都有一个运行等待队列(runqueue),CPU在运行时就是从该队列中获取进程进行调度的。

在这里插入图片描述
在运行等待队列中的进程本质上就是在等待CPU资源,实际上不止是等待CPU资源如此,等待其他资源也是如此,比如锁的资源、磁盘的资源、网卡的资源等等,它们都有各自对应的资源等待队列。
在这里插入图片描述
例如,当某一个进程在被CPU调度时,该进程需要用到锁的资源,但是此时锁的资源正在被其他进程使用:

  • 那么此时该进程的状态就会由R状态变为某种阻塞状态,比如S状态。并且该进程会被移出运行等待队列,被链接到等待锁的资源的资源等待队列,而CPU则继续调度运行等待队列中的下一个进程。
  • 此后若还有进程需要用到这一个锁的资源,那么这些进程也都会被移出运行等待队列,依次链接到这个锁的资源等待队列当中。
  • 直到使用锁的进程已经使用完毕,也就是锁的资源已经就绪,此时就会从锁的资源等待队列中唤醒一个进程,将该进程的状态由S状态改为R状态,并将其重新链接到运行等待队列,等到CPU再次调度该进程时,该进程就可以使用到锁的资源了。

总结下来就是:

  • 站在操作系统的角度,进程等待某种资源,就是将当前进程的task_struct放入对应的等待队列,这种情况可以称之为当前进程被挂起等待了。
  • 站在用户角度,当进程等待某种资源时,用户看到的就是自己的进程卡住不动了,我们一般称之为应用阻塞了。
  • 这里所说的资源可以是硬件资源也可以是软件资源,锁本质就是一种软件资源,当我们申请锁时,锁当前可能并没有就绪,可能正在被其他线程所占用,此时当其他线程再来申请锁时,就会被放到这个锁的资源等待队列当中。

死锁的四个必要条件

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

注意: 这是死锁的四个必要条件,也就是说只有同时满足了这四个条件才可能产生死锁。

避免死锁

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

除此之外,还有一些避免死锁的算法,比如死锁检测算法和银行家算法。

Linux线程同步

同步概念与竞态条件

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

首先,我们得理解,单纯的加锁并不存在什么什么问题,但是由于各个线程之间的竞争能力有所差异,可能就会存在某一个线程频繁地申请锁和释放锁,假设此时我们有一个线程A和B,线程A需要写入数据,线程B需要读取数据,此时线程A的能力强一点,他就会一直申请锁和释放锁。直到临界区数据被写满,此时线程B一直就处于阻塞状态,所以,单纯的加锁不存在什么问题,但是对于效率来说又比较影响。

那么如何解决这个问题呢?

我们增加一个规律,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后;增加这个规则之后,下一个获取到锁的资源的线程就一定是在资源等待队列首部的线程,如果有十个线程,此时我们就能够让这十个线程按照某种次序进行临界资源的访问,这就叫做同步,引入同步后该问题就能很好的解决。

条件变量

条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。

条件变量主要包括两个动作:

  • 一个线程等待条件变量的条件成立而被挂起。
  • 另一个线程使条件成立后唤醒等待的线程。

条件变量通常需要配合互斥锁一起使用。

条件变量函数

初始化

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

参数说明:

  • cond:需要初始化的条件变量。
  • attr:初始化条件变量的属性,一般设置为NULL即可。

返回值说明:

  • 条件变量初始化成功返回0,失败返回错误码。

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

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

销毁

销毁条件变量的函数叫做pthread_cond_destroy,该函数的函数原型如下:

int pthread_cond_destroy(pthread_cond_t *cond);

参数说明:

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

返回值说明:

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

销毁条件变量需要注意:

使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁。

等待条件满足

等待条件变量满足的函数叫做pthread_cond_wait,该函数的函数原型如下:

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

参数说明:

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

返回值说明:

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

唤醒等待

唤醒等待的函数有以下两个:

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

区别:

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

参数说明:

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

返回值说明:

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

下面我们用主线程创建四个新线程,让主线程控制这四个新线程活动。这四个新线程创建后都在条件变量下进行等待,直到主线程检测到键盘有输入时才唤醒一个等待线程,如此进行下去。

#include <iostream>
#include <unistd.h>
#include <cstring>#define TNUM 4
typedef void (*func_t)(const std::string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond);
volatile bool quit = false;class ThreadData
{
public:ThreadData(const std::string &name, func_t funcs, pthread_mutex_t *pmutex, pthread_cond_t *pcond): _name(name), _funcs(funcs), _mutex(pmutex), _cond(pcond){}public:std::string _name;func_t _funcs;pthread_mutex_t *_mutex;pthread_cond_t *_cond;
};void func1(const std::string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{while (!quit){//一定要在加锁和解锁之间进行waitpthread_mutex_lock(pmutex);// if(临界资源是否就绪-- 否)pthread_cond_wait(pcond, pmutex);//默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞std::cout << name << "  runing-- 下载" << std::endl;pthread_mutex_unlock(pmutex);}
}void func2(const std::string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{while (!quit){pthread_mutex_lock(pmutex);pthread_cond_wait(pcond, pmutex);//默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞std::cout << name << "  runing-- 播放" << std::endl;pthread_mutex_unlock(pmutex);}
}void func3(const std::string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{while (!quit){pthread_mutex_lock(pmutex);pthread_cond_wait(pcond, pmutex);//默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞std::cout << name << "  runing-- 刷新" << std::endl;pthread_mutex_unlock(pmutex);}
}void func4(const std::string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{while (!quit){pthread_mutex_lock(pmutex);pthread_cond_wait(pcond, pmutex);//默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞std::cout << name << "  runing-- 扫描用户信息" << std::endl;pthread_mutex_unlock(pmutex);}
}void *Entry(void *args)
{ThreadData *td = (ThreadData *)args;td->_funcs(td->_name, td->_mutex, td->_cond);delete td;pthread_exit((void*)0);
}
int main()
{// 创建锁pthread_mutex_t mutex;pthread_cond_t cond;// 初始化锁pthread_mutex_init(&mutex, nullptr);pthread_cond_init(&cond, nullptr);// 创建线程pthread_t tid[TNUM];func_t funcs[TNUM] = {func1, func2, func3, func4};for (int i = 0; i < TNUM; i++){std::string name = "thread";name += std::to_string(i + 1);ThreadData *td = new ThreadData(name, funcs[i], &mutex, &cond);pthread_create(tid + i, nullptr, Entry, (void *)td);}int cnt = 10;while(cnt){std::cout << "resume thread run code ...." << cnt-- << std::endl;pthread_cond_signal(&cond);sleep(1);}std::cout << "ctrl done" << std::endl;quit = true;pthread_cond_broadcast(&cond);//主线程进程等待for(int i = 0; i < TNUM; i++){pthread_join(tid[i], nullptr);std::cout << "thread[" << tid[i] << "] quit!!!" << std::endl;}pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);return 0;
}

此时我们会发现唤醒这4个线程时具有明显的顺序性,根本原因是当这若干个线程启动时默认都会在该条件变量下去等待,而我们每次都唤醒的是在当前条件变量下等待的头部线程,当该线程执行完打印操作后会继续排到等待队列的尾部进行wait,所以我们能够看到一个周转的现象。
在这里插入图片描述
如果我们想每次唤醒都将在该条件变量下等待的所有线程进行唤醒,可以将代码中的pthread_cond_signal函数改为pthread_cond_broadcast函数。

此时我们每一次唤醒都会将所有在该条件变量下等待的线程进行唤醒,也就是每次都将这4个线程唤醒。
在这里插入图片描述

相关文章:

Linux线程安全

线程安全 Linux线程互斥进程线程间的互斥相关背景概念互斥量mutex互斥量的接口互斥量实现原理探究 可重入VS线程安全概念常见的线程不安全的情况常见的线程安全的情况常见的不可重入的情况常见的可重入的情况可重入与线程安全联系可重入与线程安全区别 常见锁概念死锁死锁的四个…...

Windows安装Node.js

1、Node.js介绍 ①、Node.js简介 Node.js是一个开源的、跨平台的JavaScript运行环境&#xff0c;它允许开发者使用JavaScript语言来构建高性能的网络应用程序和服务器端应用。Node.js的核心特点包括&#xff1a; 1. 事件驱动: Node.js采用了事件驱动的编程模型&#xff0c;通…...

想要开发一款游戏, 需要注意什么?

开发一款游戏是一个复杂而令人兴奋的过程。游戏开发是指创建、设计、制作和发布电子游戏的过程。它涵盖了从最初的概念和创意阶段到最终的游戏发布和维护阶段的各个方面。 以下是一些需要注意的关键事项&#xff1a; 游戏概念和目标&#xff1a; 确定游戏开发的核心概念和目标…...

横向AlGaN/GaN基SBD结构及物理模型数据库的开发

GaN基功率器件凭借其临界电场高、电子饱和漂移速度大、热导率高等优良性能在大功率快充、充电桩、新能源汽车等领域具备广泛应用空间。为进一步助推半导体高频、高功率微电子器件的发展进程&#xff0c;天津赛米卡尔科技有限公司技术团队依托先进的半导体TCAD仿真平台成功开发出…...

使用安卓Termux+Hexo,手机也能轻松搭建个人博客网站

文章目录 前言1.安装 Hexo2.安装cpolar3.远程访问4.固定公网地址5.结语 前言 Hexo 是一个用 Nodejs 编写的快速、简洁且高效的博客框架。Hexo 使用 Markdown 解析文章&#xff0c;在几秒内&#xff0c;即可利用靓丽的主题生成静态网页。 下面介绍在Termux中安装个人hexo博客并…...

Spring在业务中常见的使用方式

目录 通过IOC实现策略模式 通过AOP实现拦截 通过Event异步解耦 通过Spring管理事务 通过IOC实现策略模式 很多时候&#xff0c;我们需要对不同的场景进行不同的业务逻辑处理举个例子&#xff0c;譬如不同的场景需要不同支付方式&#xff0c;普通的逻辑是使用if-else&#x…...

PyTorch 深度学习实战

文章目录 前言1. 环境安装1.Anaconda2.pytorch cuda 环境3.测试 前言 1. 环境安装 1.Anaconda 可以参考这里&#xff1a;Anaconda学习 2.pytorch cuda 环境 我是按照下面的博客一步步完成&#xff0c;亲测有效 Pytorch安装教程&#xff08;最全最详细版&#xff09; 我的…...

学生用RockyLinux9.2模板虚拟机说明

“RockyLinux9.2”模板虚拟机下载地址 链接&#xff1a;https://pan.baidu.com/s/1xcakszIQ7Kp9Nw_NA9Znlg?pwdqzmm 提取码&#xff1a;qzmm 1.模板机是基于“Rocky-9.2-x86_64-minimal.iso”安装 2.模板机是2023.10.10执行dnf update && dnf upgrade更新和升级软件…...

BUUCTF reverse3 1

先运行下 看来是输入正确的flag 使用DIE查看文件 看起来没有壳&#xff0c;直接IDA打开 shift F12查找字符串 一路跟踪 到汇编窗口后F5 这里对Destination和Str2进行比较&#xff0c;Str2有值那么Str2就是经过上面一系列处理之后得到的内容了 继续分析上面的代码 根据…...

关于webWorker未解问题

今天尝试学习webworker,尝试在vue3项目里面使用 使用的就是常规方法,使用worker-loader,加上在vue.config.js内部添加配置 使用完发现问题 如图所见,该worker仅仅配置点击后传输字符串"1",并在worker内部打印,发现打印不出来 但是仅仅只是将引入的文件换个名字 …...

自然语言处理(NLP)的开发框架

自然语言处理&#xff08;NLP&#xff09;领域有许多开源的框架和库&#xff0c;用于处理文本数据和构建NLP应用程序。以下是一些常见的NLP开源框架及其特点&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合…...

Spring Boot与Kubernetes结合:构建高可靠、高性能的微服务架构

Spring Boot和Kubernetes&#xff08;K8s&#xff09;是当今非常热门的技术&#xff0c;它们的结合可以帮助开发者更高效地构建、部署和管理应用程序。本文将详细介绍Spring Boot和Kubernetes的主要特点&#xff0c;以及它们结合使用的优势。 一、Spring Boot的特点 Spring B…...

Qt自带的日志重定向机制

//Qt5开始提供了日志上下文信息输出&#xff0c;比如输出当前打印消息所在的代码文件、行号、函数名等。 //如果是release还需要在pro中加上 DEFINES QT_MESSAGELOGCONTEXT 才能输出上下文&#xff0c;默认release关闭的。 //切记不要在日志钩子函数中再写qdebug之类的&#x…...

笔记36:CNN的多通道卷积到底是什么样的

总结&#xff1a; &#xff08;1&#xff09;输入卷积层的feature_map的通道数&#xff0c;就是该卷积层每个卷积核所含有的通道数 &#xff08;2&#xff09;输出卷积层的feature_map的通道数&#xff0c;就是该卷积层所含有的卷积核的个数 a a a a 解释&#xff1a;【…...

【eigen】解决报错 return type of RCmpEQReturnType Eigen::operator== is not ‘bool’

文章目录 1. 发现问题2. GPT 分析问题3. 解决问题 1. 发现问题 在 c 项目中使用了 C 20 编译标准&#xff08;GCC10&#xff09;&#xff0c;结果 eigen 中出现报错 /usr/include/eigen3/Eigen/src/Cholesky/LDLT.h:372:58: error: return type of ‘const RCmpEQReturnType …...

QT 实现mysql的长连接

以下是一个使用Qt进行MySQL长连接的示例程序: #include <QCoreApplication> #include <QSqlDatabase> #include <QSqlQuery> #include <QDebug>class MySQLConnection :...

Vue 使用 setup 语法糖

setup 语法糖在书写上更加方便简洁&#xff0c;可以直接在 script 标签中书写 setup 的内容&#xff0c;并且无需使用 return 返回。 基础使用&#xff1a; <script setup></script> 注&#xff1a;setup 语法糖中不能存在 export default {} &#xff0c;否则会…...

Feign(替代RestTemplate)远程调用

Feign初步学习 定义 Feign 是一个基于 Java 的 HTTP 客户端库&#xff0c;它是 Spring Cloud 中的一部分&#xff0c;用于简化微服务之间的 HTTP 通信。与传统的使用 RestTemplate 来调用 RESTful 服务不同&#xff0c;Feign 提供了一种声明式、基于接口的方式来定义和调用 H…...

查找算法 —— 斐波拉契查找法

一、介绍 斐波拉契查找法是以分割范围进行查找的&#xff0c;分割的方式是按照斐波拉契级数的方式来分割。好处是&#xff1a;只用到加减运算&#xff0c;计算效率较高一些。 要使用斐波拉契查找首先需要定义一颗斐波拉契查找树&#xff0c;建立规则如下&#xff1a; 1.斐波拉契…...

PL/SQL全量同步

全量同步 -- 实现逻辑:用源表的数据直接覆盖目标表 -- 插入数据前:先清空目标表,然后查询源表的数据,插入目标表 -- 1. 先创建一个目标表 CREATE TABLE EMP_T AS SELECT E.*, SYSDATE CREATE_DATE, SYSDATE UPDATE_DATE, SYSDATE ETL_DATE FROM EMP E WHERE 12;SELECT * FR…...

IO类型游戏研发定制开发

"IO" 类型的游戏开发通常是指那些在线多人游戏&#xff0c;其特点是快节奏、实时互动和简单的玩法。这些游戏的名字通常以 ".io" 结尾&#xff0c;如 "Agar.io"、"Slither.io" 等。如果您有兴趣进行 "IO" 类型游戏的研发或提…...

Eclipse iceoryx(千字自传)

1 在固定时间内实现无任何限制的数据传输 在汽车automotive、机器人robotics和游戏gaming等领域,必须在系统的不同部分之间传输大量数据。使用Linux等操作系统时,必须使用进程间通信(IPC)机制传输数据。Eclipse iceoryx是一种中间件,它使用零拷贝Zero-Copy、共享内存Share…...

竞赛 深度学习 opencv python 公式识别(图像识别 机器视觉)

文章目录 0 前言1 课题说明2 效果展示3 具体实现4 关键代码实现5 算法综合效果6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于深度学习的数学公式识别算法实现 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学…...

Pikachu靶场——跨站请求伪造(CSRF)

文章目录 1. 跨站请求伪造&#xff08;CSRF&#xff09;1.1 CSRF(get)1.2 CSRF(post)1.3 CSRF Token1.4 CSRF漏洞防御 1. 跨站请求伪造&#xff08;CSRF&#xff09; 还可以参考我的另一篇文章&#xff1a;跨站请求伪造(CSRF) 全称Cross-site request forgery&#xff0c;翻译…...

软件测试简历项目经验怎么写?大厂面试手拿把掐

前言 在写简历之前&#xff0c;我们先来看看失败者的简历和成功者的简历之间有什么区别。为什么成功者的简历可以在求职中起到“四两拨千斤”的作用&#xff0c;而失败者的简历却被丢进了垃圾桶&#xff0c;这两者到底有什么不同&#xff1f; 成功的简历与失败的简历 我们发现…...

图像处理与计算机视觉--第七章-神经网络-单层感知器

文章目录 1.单层感知器原理介绍2.单层感知器算法流程3.单层感知器算法实现4.单层感知器效果展示5.参考文章与致谢 1.单层感知器原理介绍 1.单层感知器是感知器中最简单的一种&#xff0c;有单个神经元组成的单层感知器只能用于解决线性可分的二分性问题2.在空间中如果输入的数据…...

pyserial,win11,串口总是被占用

之前哪里看到的忘记了&#xff0c;记录&#xff1a; win11&#xff0c;用pyserial这个库&#xff0c;打开COM后&#xff0c;程序退出&#xff0c;关闭串口&#xff0c;下次打开仍然会报错。每次都要拔串口线&#xff0c;很烦。 去设备管理器里&#xff0c;把usb串口线的驱动页…...

网站上线如何检查?

网站上线如何检查?很多企业搭建好网站之后&#xff0c;不知道如何检查网站&#xff0c;其实网站上线之后&#xff0c;要对网站的代码&#xff0c;网站的SEO细节&#xff0c;等重要因素检查&#xff0c;下面我们就来讲述一下企业优化网站建站、上线检查要求。 网站上线如何检查…...

如何理解pytorch中的“with torch.no_grad()”?

torch.no_grad()方法就像一个循环&#xff0c;其中循环中的每个张量都将requires_grad设置为False。这意味着&#xff0c;当前与当前计算图相连的具有梯度的张量现在与当前图分离了我们将不再能够计算关于该张量的梯度。直到张量在循环内&#xff0c;它才与当前图分离。一旦用梯…...

Linux虚拟机克隆之后使用ip addr无法获取ip地址

Linux虚拟机克隆之后使用ip addr无法获取ip地址 因为克隆得到的虚拟机&#xff0c;与原先的linux系统是一模一样的包括MAC地址和IP地址。需要修改信息。 设置IP地址&#xff1a; 使用vi命令打开linux的网卡 //ifcfg-enth0是虚拟网卡的名称&#xff0c;如果你的不叫这个名字&a…...