【Linux 系统】多线程(线程控制、线程互斥与同步、互斥量与条件变量)-- 详解(下)

简介: 【Linux 系统】多线程(线程控制、线程互斥与同步、互斥量与条件变量)-- 详解(下)

【Linux 系统】多线程(线程控制、线程互斥与同步、互斥量与条件变量)-- 详解(中)https://developer.aliyun.com/article/1515719?spm=a2c6h.13148508.setting.29.11104f0e63xoTy

五、线程互斥

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

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

(1)模拟抢票逻辑

或者也可以说 if 判断条件为真后,代码可能并发切换到其它线程,因为 usleep 漫长的等待过程中可能会有多个线程进入该临界区,所以 tickets 就不是原子的。实际多线程在切换时极有可能出现因为数据交叉操作而导致数据不一致的问题,那么 OS 中可能从内核态返回用户态的时候就会进行线程间切换。


A. 代码

如下图,线程 A 将 tickets:1000 从内存读到 CPU,然后 ticket--。因为某种原因,还没等线程 A 把 tickets 写回内存,就被 CPU 剥离,在剥离之前 CPU 上一定有线程 A 的临时数据或者说是上下文数据,然后将上下文数据保存在线程内部,紧接着 CPU 开始调度线程 B,线程 B 将 tickets:1000 从内存读到 CPU。因为线程 B 的运气比较好,所以 tickets-- 了 500 次,然后因为时间片到了,就把 tickets:500 写回内存,然后 CPU 继续调度线程 A,此时将线程 A 中保存的上下文数据恢复到 CPU 对应的寄存器中,再继续执行第 3 步,把 tickets:999 写回物理内存。


B. 分析
a. 出现原子性问题

当第 1 个线程 if 判断成功后,执行到 usleep,陷入内核休眠并执行第 2 个线程,以此类推。后来第 1 个线程醒来后,输出并执行 tickets--,以此类推。这样就有可能出现多个线程都在 if 判断中,这样就有可能会出现减到负数的情况。要解决这种问题,所以就引出了线程互斥。


b. 未出现原子性问题


2、互斥量 mutex

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

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。
为什么可能无法获得争取结果?  
  • if 语句判断条件为真以后,代码可以并发的切换到其他线程。
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  • --ticket 操作本身就不是一个原子操作。

取出 ticket-- 部分的汇编代码,操作并不是原子的,而是对应三条汇编指令:

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

要解决以上问题,需要做到三点:

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

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


3、互斥量的接口

  • mutex:初始化或释放的锁。
  • attr:锁的属性,设置nullptr,即不管。


(1)初始化互斥量

A. 静态分配(初始化不用 destroy)


B. 动态分配


(2)销毁互斥量

销毁互斥量需要注意:

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

(3)互斥量加锁和解锁


A. 互斥量加锁

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

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

B. 互斥量解锁


这里就完成了线程互斥的操作。线程是绝对可以随时被切换的,但是线程是 “拿着锁” 走的,线程不回来,锁没法释放,期间任何线程不在的时候,当然也可以申请锁,只不过不会成功。对其它拥有锁的线程,执行临界区的时候,要么不执行(没有申请锁),要么执行完毕(释放锁),这就间接完成了原子性。此外定义的全局 lock,一定需要被所有线程先看到,所以本质 lock 也是一种临界资源,难道再加一层锁吗?那谁又来保护它呢?所以只需要保证 lock,unlock 时是原子的即可,也就是说在这过程中只会有一个线程对 lock 变量进行操作。

打开加解锁,运行速度明显变慢了,且因为互斥,没有出现原子性问题:

错误写法:

注意:这样做是有问题的,因为一个线程已经锁定互斥量,其它线程申请互斥量,但没有竞争到互斥量,那么 pthread_lock 调用时会陷入阻塞,看到的现象就是执行流被挂起,等待直到互斥量解锁。也就是说这里当一个线程执行到 tickets == 0 时,会走到 else,break 出循环,此时加锁却没有解锁,现在会一直处于阻塞状态,其它线程都在等待。所以,正确写法应该是上面运行的代码。


【改进抢票逻辑完整代码】

加锁就是串行执行吗?

是的,执行临界区代码一定是串行的。

加锁之后,线程在临界区中会切换,会有问题吗?

不会有问题。虽然线程被切换了,但是是在持有锁的情况下被切换的,所以其它抢票线程要执行临界区代码,也必须先申请锁,但它是无法申请成功的。所以,也不会让其他线程进入临界区,也就保证了临界区中数据的一致性。

如果线程不申请锁,而只是单纯的访问临界资源呢?

不被允许,这是错误的编码方式。  

在没有持有锁的线程看来,对它最有意义的两种情况:

  1. 线程 1 没有持有锁。
  2. 线程 1 释放锁,此时没有持有锁的线程可以申请锁。

要访问临界资源,那么每一个线程都必须实现申请锁,每一个线程必须都得看到同一把锁,且去访问它。锁本身就是一种共享资源,那么为了保证锁的安全,申请和释放锁必须都是原子的。


(4)互斥量实现原理探究

经过上面的例子,我们已经能够意识到单纯的 i++ 或者 ++i 都不是原子的,因为这有可能会出现数据不一致问题。为了实现互斥锁操作,大多数体系结构都提供了 swap exchange 指令,该指令的作用是把 CPU 寄存器区和内存单元的数据相交换,由于只有一条指令,就保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。也就是说锁的原子性实现是由寄存器级别的,如下是 lock 和 unlock 的伪代码以及理解:

lock:
  movb $0, %al
  xchgb %al, mutex
  if(a1寄存器的内容 > 0){
    return 0;
  }
  else{
    挂起等待; 
  } 
  goto lock;
unlock:
  movb $1, mutex
  唤醒等待mutex的线程;
  return 0;

mutex 就是一个内存中的变量,和定义 int 没太大区别,假设 mutex 默认是 1。

线程 X 执行 movb $0,%al 将 %al 寄存器清零。在这个过程中线程 Y 当然也有可能执行这条语句,这没问题,因为线程在剥离时会作上下文数据的保存,线程在切换时就会把上下文数据保存于 TCB 中。

线程 X 执行 xchgb %al,mutex 将寄存器 %al 中的值 0 和内存的值 mutex:1 交换,这意味着 mutex 里面的值被交换到了对应线程中的上下文中,就相当于变成线程私有的。是的,在汇编代码上只要一条语句就完成交换了,因为它只是一条语句,所以交换过程一定是原子的。具体 xchgb 是怎么完成的,可以去了解了解它的汇编原理,简单提一下就是在体系结构上有一个时序的概念,在一个指定周期中,去访问总线时,汇编指令能被 CPU 接收到,是因为汇编指令会在特定的时间放在总线上的,而总线是可以被锁住的,这在硬件上实现比较简单,所以即使 xchgb 汇编翻译成二进制的时候,是对应多条语句的,但是它依旧不影响,因为硬件级别把总线锁住了,所以它虽然在汇编翻译成二进制时依旧是有多条语句的,但是因为总线被锁住了,所以不会出现原子性问题。

当线程 X 交换完后,线程 Y 突然切换进来了,在此之前就会把线程 X 的上下文数据保存于线程 X 的 TCB 中,然后把线程 Y 中上一次在 TCB 中保存的上下文数据恢复到 CPU(这里没有),然后线程 Y 执行 movb $0,%al 将 %al 寄存器清零,执行 xchgb %al,mutex 将寄存器 %al 中的值 0 和内存的值 mutex:0 交换。所以线程 Y 再往下走就会 else 挂起等待。

再线程切换到线程 X,把线程 Y 中的上下文数据保存于自己的 TCB,然后线程 X 把自己在 TCB 中保存的上下文数据数据恢复到 CPU,再执行 if,就可以访问临界区中的代码了,然后访问临界区成功,并返回。

然后线程 X 去执行 unlock 中的 movb $1,mutex 把内存中的 mutex 值又置为 1,然后唤醒等待 mutex 的线程 Y,成功返回,这样线程 X 就完成了解锁,unlock 也一定是原子的,因为能执行 unlock 的一定是曾经 lock 过的,所以 unlock 是不是原子性的已经不是重点了。最后切换到线程 Y,将上下文数据恢复,继续往下执行 goto lock,将寄存器 %al 清零, 然后将其与内存中的 mutex 值交换,成功访问临界区资源返回,然后把 mutex 值置为 1,没有唤醒的 mutex 线程,然后成功解锁。注意这里以不用管寄存器中的 %al:1,因为下次在申请锁时会先把 %al 置 0。

注意

  1. 这里的 %al 是寄存器数据,是线程的上下文数据,代表的是线程私有。
  2. mutex 互斥锁,本质就是内存中的一个内存空间,空间里面的值是 1,是可以被所有的线程读取并访问的。
  3. 综上,在交换的时候能保证只有一个 1。为什么一定要 swap/exchange 或者 xchgb 呢,mov %al, mutex 不行吗?一定不行,虽然 mov 也是一条汇编,但是它是拷贝,和交换是不一样的,拷贝会把 mutex 的值拷贝到寄存器中,这样一来,就不能保证只有一个 1 了,其它线程依然可以在一个线程正在访问临界区时也访问临界区,所以需要 swap/exchange 或者 xchgb 指令,以保证在多线程执行时,每个线程的上下文中,只有一个 1,这样就可以保证了原子了。

(5)可重入 & 线程安全

A. 重入和线程安全的概念

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

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


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

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

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

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

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

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

六、常见锁概念

1、死锁

(1)概念

死锁是一种常见的锁,死锁是指在一组进程或线程中的各个进程或线程均占有不会释放的资源,它们申请锁而不释放锁,进而导致多个执行流互相等待的状态。

死锁通常发生在两个或多个进程或线程之间竞争资源的情况,当一个进程或线程持有一个资源并请求另一个进程或线程持有的资源时,如果另一个进程或线程也持有该进程所需的资源并请求第一个进程或线程的资源,就有可能发生死锁。

举个例子,小明和小红身上各有 5 毛钱,想买 1 块钱的辣条,小明对小红说把你的 5 毛钱给我,我来买,小红也对小明说把你的 5 毛钱给我,我来买,此时他们既想要申请对方的 5 毛钱,又不想妥协释放自己的 5 毛钱,这种状态就叫做死锁。


(2)死锁四个必要条件

只要产生了死锁,一定有如下四个条件:

  1. 互斥条件:一个资源每次只能被一个执行流使用。(本质上引入互斥就是为了保护临界资源,由并行变成了串行,保证了原子性。但也带了新的问题:死锁。因为互斥只要存在就会产生申请资源阻塞的情况,就有可能出现死锁
  2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。(其实就是请求你的锁给我,我的锁不释放
  3. 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。(一般而言,你要我的资源,而我要别人的资源,是不会造成死锁的。而你要我的资源,我也要你的资源,就可能会造成死锁,因为我们是竞争关系

(3)避免死锁

  1. 破坏死锁的四个必要条件其中一个。(互斥条件很难被破坏,因为它要求一次只有一个进程或线程使用资源,一般也建议能不用锁就不用锁;请求与保持就是释放相对应的锁资源;不剥夺条件就是可以根据优先级设计一个算法,就算一个执行流未使用完资源,也可以被剥夺;循环等待条件就是若干执行流之间在申请和释放锁资源时不要形成回路
  2. 加锁顺序一致。
  3. 避免锁未释放的场景。
  4. 资源一次性分配。

(4)避免死锁算法

A. 死锁检测算法(了解)

死锁检测算法是一种利用进程状态的变化来识别死锁的方式,能够提高系统的并发性和效率。

这种算法主要有两种形式:

  1. 预防式,它在进程申请资源时就进行干预,以防止系统进入不安全状态;
  2. 事后处理,它允许系统进入不安全状态,然后通过某种算法检测并恢复。

具体来说,死锁检测需要用某种数据结构来保存资源的请求和分配信息,并提供一种算法,利用上述信息来检测系统是否已进入死锁状态。如果检测到系统已经发生死锁,可以采取相应的措施解除死锁,如资源剥夺法、撤销进程法或进程回退法等。值得注意的是,死锁检测通常在资源有向图(也称为资源分配图)上进行。

因此,理解和掌握如何有效地在资源有向图上进行死锁检测和解除,对于提高系统的性能和稳定性至关重要。


B. 银行家算法(了解)

银行家算法,是由艾兹格·迪杰斯特拉在 1965 年为 T.H.E 系统设计的一种避免死锁产生的算法。这个算法不仅适用于操作系统给进程分配资源,还可以用于其他需要进行资源分配和调度的场景。

该算法主要使用了四个数据结构:可利用资源向量 Available最大需求矩阵 Max分配矩阵 Allocation 以及需求矩阵 Need。这些数据结构分别记录了系统中当前可用的资源数量、每个进程对资源的最大需求量、系统已经分配给每个进程的资源数量以及每个进程还需要的资源数量。

在实现的过程中,银行家算法首先检查系统是否处于安全状态,如果是,则允许进程继续申请资源;否则,将拒绝进程的请求。如果系统进入不安全状态,则需要采取相应的措施使系统返回到安全状态。

总的来说,银行家算法是一种既简单又有效的死锁避免方法,它通过合理的资源分配和调度来防止系统进入不安全状态,从而避免了死锁的发生。


七、Linux 线程同步

很明显,线程同步和线程互斥是相对的。对于前面的模拟抢票逻辑的实现,当然可能存在某个线程竞争锁的能力特别强,每次申请都是它优先申请到锁资源,其它线程没有机会拥有锁,我们称这种问题为饥饿问题,这样就会出现该线程一直在循环进行申请锁、检测(抢票)、释放锁,即使票已经没有了,因为释放锁后并没有 break 出循环。

饥饿问题本身并没有错,只是说不合理。就好比如你从墙边拿了钥匙去单间自习室中自习,一会出来了,刚把钥匙挂在墙边,看到有很多人在排队,你想着好不容易抢到了单间自习室,于是你刚放下钥匙又拿起了钥匙进去自习,一会又出来放钥匙了,看到人还是很多,心想把明早的也自习算了,于是又拿着钥匙去自习了,反反复复。你本质能反反复复的进去自习是因为你离钥匙最近,理论上虽然没问题,但却不合理。所以你作为自习室的管理者,就规定了如果从单间自习室出来,把钥匙挂在墙上后,不能立即再申请钥匙,如果你要再次申请,就必须排到队列的尾部。

排队的本质就是让我们获取锁在安全的前提下,按照某种顺序进行申请和释放,这就叫做线程同步的过程。同步不是严格意义上解决数据正确或错误的问题,而是解决不合理的问题。所以我们经常会一起听到互斥和同步这两个概念,因为光有互斥还无法保证执行流进行合理的临界资源访问。


1、条件变量

(1)概念

条件变量是一个原生线程库提供的一个描述临界资源状态的对象或变量

在抢票时发现票已经售完了,就不应该再申请锁了,而应该等有票了再申请。比如说,中午你问你妈有没有吃的,你妈说没有,过了两秒,你又问你妈有没有吃的,你妈又说没有,反反复复,这样当然没错,只是不合理。实际上你妈告诉你没有吃的的时候,就等同于临界资源中没有票了,而合理的是,你不要着急的去问你妈或申请锁,而应该跟你妈说等会有吃的了叫我一声,然后你就在一旁等待,直到你妈唤醒你,然后你再去询问。所以需要条件变量来描述临界资源状态,之前之所以不断的轮询申请锁、检测、释放锁,本质就是我们并不知道临界资源的状态。

如下图,当右边正在放苹果时,左边的来拿了,此时左侧的人不一定能拿到苹果,因为正在放这个动作包含了放前、放中、放后,所以这就叫做二义性问题。所以使用锁来解决这种问题,无论你要放还是要拿苹果都需要加锁,其过程是原子的,这样就解决了二义性问题。那么问题又来了,拿苹果的比较磨蹭,放苹果的又比较利索,然后放苹果的加锁,再放苹果,接着解锁,假设两个人都是瞎子,那放苹果的也并不知道苹果有没有被拿苹果的拿走,所以放苹果的又开始加锁,然后检测到苹果没有被拿走,接着又释放锁,拿苹果的人实在是太慢了,放苹果的人反反复复加锁、检测、解锁了很多次依旧检测到盒子里的苹果,没有任何有效的动作,放苹果的人的这样一个周期很快,而导致拿苹果的人就算要去拿了也竞争不过放苹果的人。以上帝视角来看,放苹果的人就是在不断的申请释放,而拿苹果的人想拿却竞争不过放苹果的人。反复强调了这种现象当然没有错,只是不合理,所以合理的是放苹果的人加锁、放苹果、解锁、然后敲一下铃铛,就去睡觉了,此时拿苹果的人就知道了(即使拿苹果的人很慢,但他一定可以拿到苹果),一定时间后,拿苹果的人开始加锁、拿苹果、解锁,敲铃铛,也去睡觉了,那么这个时候放苹果的人也知道拿苹果的人把苹果拿走了,放苹果的人就可以继续的往盒子里放苹果了,这里的铃铛被称为条件变量,铃铛就是描述盒子的状态,它对应的就是临界资源,所以条件变量就是描述临界资源的状态。所以,有了条件变量就可以不用频繁的通过申请和释放锁的方式,以达到检测临界资源的目的。

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

2、同步概念与竞态条件

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

3、条件变量函数

(1)接口介绍

A. 初始化

初始化 cond,并设置属性 attr。

  • cond:要初始化的条件变量。
  • attrNULL。


B.  销毁

销毁条件 cond。


C. 等待条件满足
  • timedwait:指定的时间范围内等待条件变量下的线程(少用)。
  • wait:指定的条件变量下进行等待,一定要入 cond 的队列,mutex 用于在锁中阻塞挂起时会自动释放锁,唤醒时会自动获取锁(后面生产者消费者中详细介绍)。
  • cond:要在这个条件变量上等待。
  • mutex:互斥量,后面详细解释。


D. 唤醒等待
  • signal:唤醒指定条件变量下等待的一个线程。
  • broadcast:唤醒所有在该条件变量下等待的线程(少用)。


4、为什么 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 将错过这个信号,可能会导致线程永远阻塞在这pthread_ cond_ wait,所以解锁和等待必须是一个原子操作。
  • int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,会去看条件量是否等于 0。如果等于,就把互斥量变成 1,直到 cond_ wait 返回,把条件量改成1,把互斥量恢复成原样。

5、条件变量使用规范

(1)等待条件代码

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

(2)给条件发送信号代码

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

【代码】


相关文章
|
4天前
|
Linux Shell 网络安全
Kali Linux系统Metasploit框架利用 HTA 文件进行渗透测试实验
本指南介绍如何利用 HTA 文件和 Metasploit 框架进行渗透测试。通过创建反向 shell、生成 HTA 文件、设置 HTTP 服务器和发送文件,最终实现对目标系统的控制。适用于教育目的,需合法授权。
28 9
Kali Linux系统Metasploit框架利用 HTA 文件进行渗透测试实验
|
27天前
|
缓存 Java Linux
如何解决 Linux 系统中内存使用量耗尽的问题?
如何解决 Linux 系统中内存使用量耗尽的问题?
115 48
|
15小时前
|
存储 缓存 监控
Linux缓存管理:如何安全地清理系统缓存
在Linux系统中,内存管理至关重要。本文详细介绍了如何安全地清理系统缓存,特别是通过使用`/proc/sys/vm/drop_caches`接口。内容包括清理缓存的原因、步骤、注意事项和最佳实践,帮助你在必要时优化系统性能。
78 62
|
1天前
|
Ubuntu Linux C++
Win10系统上直接使用linux子系统教程(仅需五步!超简单,快速上手)
本文介绍了如何在Windows 10上安装并使用Linux子系统。首先,通过应用商店安装Windows Terminal和Linux系统(如Ubuntu)。接着,在控制面板中启用“适用于Linux的Windows子系统”并重启电脑。最后,在Windows Terminal中选择安装的Linux系统即可开始使用。文中还提供了注意事项和进一步配置的链接。
10 0
|
24天前
|
Ubuntu Linux 网络安全
linux系统ubuntu中在命令行中打开图形界面的文件夹
在Ubuntu系统中,通过命令行打开图形界面的文件夹是一个高效且实用的操作。无论是使用Nautilus、Dolphin还是Thunar,都可以根据具体桌面环境选择合适的文件管理器。通过上述命令和方法,可以简化日常工作,提高效率。同时,解决权限问题和图形界面问题也能确保操作的顺利进行。掌握这些技巧,可以使Linux操作更加便捷和灵活。
17 3
|
29天前
|
存储 运维 Linux
如何在 Linux 系统中使用 envsubst 命令替换环境变量?
`envsubst` 是 Linux 系统中用于替换文本中环境变量值的实用工具。本文分三部分介绍其工作原理、使用方法及实际应用,包括配置文件替换、脚本执行中环境变量替换和动态生成文件等场景,帮助用户高效利用 `envsubst` 进行开发和运维工作。
49 4
|
27天前
|
Linux
在 Linux 系统中,`find` 命令
在 Linux 系统中,`find` 命令
28 1
|
27天前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
56 1
|
27天前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
12天前
|
存储 Oracle 安全
服务器数据恢复—LINUX系统删除/格式化的数据恢复流程
Linux操作系统是世界上流行的操作系统之一,被广泛用于服务器、个人电脑、移动设备和嵌入式系统。Linux系统下数据被误删除或者误格式化的问题非常普遍。下面北亚企安数据恢复工程师简单聊一下基于linux的文件系统(EXT2/EXT3/EXT4/Reiserfs/Xfs) 下删除或者格式化的数据恢复流程和可行性。