在这里插入图片描述

上一篇讨论临界区时,很多方案本质上都是忙等:拿不到锁就不断循环检查。它们能说明互斥为什么困难,却不适合等待时间未知的场景。因为一个线程什么也没做,却始终占着 CPU;更糟的是,在优先级调度下,它还可能妨碍真正持锁的线程继续运行。

这一篇从生产者—消费者问题出发,梳理信号量、互斥量、Futex 以及 Pthreads 条件变量之间的关系。重点不是记住某几个函数,而是分清两类完全不同的需求:

  • 互斥:同一时刻只能有一个执行流修改共享数据;
  • 同步:某件事必须等到某个条件成立后才能继续。

生产者和消费者同时需要这两件事,因此它是理解同步原语最经典的模型。

一、问题引入:为什么不能一直轮询加锁?

1. 优先级调度与饥饿

优先级调度的直觉很简单:优先级高的进程先运行。但如果系统总有高优先级任务到来,低优先级任务可能长期得不到 CPU,这种现象叫作饥饿(starvation)

例子

优先级:谁的优先级高,执行谁

如果A一直运行,那么B始终得不到CPU资源,B可能出现"饿死"的问题

在这里插入图片描述

解决方法:例如Linux系统中,会规定 “高优先级的队列在某个时间端内只能调度x次”

操作系统通常不会让高优先级队列无限制地独占处理器。例如会结合时间片、老化(aging)或配额策略,让等待足够久的任务也能被调度。这里要记住:调度器解决的是“谁获得 CPU”的问题,而锁解决的是“谁能访问共享资源”的问题;两者一旦交织,就会产生更复杂的现象。

2. 优先级反转

设有三个线程:高优先级线程 H、中优先级线程 M、低优先级线程 L

  1. L 先获得一把锁,正在访问临界区;
  2. H 到来,也需要这把锁,因此被阻塞;
  3. M 不需要锁,但优先级高于 L,于是不断抢占 L

结果是:H 在等待 L 释放锁,可 L 又一直得不到运行机会。高优先级线程间接被中优先级线程拖住,这就是优先级反转(priority inversion)

把忙等改为阻塞等待,可以避免 H 空转浪费 CPU;但它本身并不能彻底消除优先级反转。常见的进一步处理是优先级继承:当 H 等待 L 持有的锁时,临时把 H 的优先级提升给 L,让 L 尽快运行、释放锁,再恢复原优先级。

无论采用什么调度策略,等待线程都不应该无限制地轮询。较长的等待应当进入等待队列,主动让出 CPU;资源可用时,再由释放者或内核唤醒它。

在这里插入图片描述

这里的难点在于:检查条件、进入等待队列与唤醒操作必须正确配合,不能在中间留下“丢失唤醒”的窗口。

二、生产者—消费者:一个有界缓冲区里的两类约束

1. 有界缓冲区模型

生产者—消费者问题又称有界缓冲区(bounded-buffer)问题。共享缓冲区有固定容量 N

  • 生产者把数据放入缓冲区;缓冲区满时,生产者必须等待;
  • 消费者从缓冲区取数据;缓冲区空时,消费者必须等待;
  • 无论是放入还是取出,都要修改缓冲区及其计数器,因此不能并发进行。

count 表示当前已有的数据项数量,则它始终应满足:

0 <= count <= N

从这里已经能看出两类约束:

  • count == N 时不能再生产、count == 0 时不能再消费,这是同步约束
  • 修改 count、读写缓冲区槽位必须串行,这是互斥约束

2. 只有 sleep/wakeup 的朴素写法

下面看一个生产者消费者的伪代码示例,这是一个典型的模型:

#include <stdbool.h> // 引入布尔类型支持  

/* 缓冲区 slot 槽的数量 */  
#define N 100  

/* 缓冲区数据的数量,使用 volatile 关键字防止编译器优化掉不必要的读取 */  
volatile int count = 0;  

// 模拟生成数据项的函数(示例) 
int produce_item(void) {  
  // 这里应该实现生成数据项的逻辑,这里简单返回静态值作为示例  
  static int item = 0;  
  return ++item;  
}  

// 模拟消费数据项的函数(示例)  
void consumer_item(int item) {  
  // 这里应该实现消费数据项的逻辑,这里仅打印作为示例  
  printf("Consumed: %d\n", item);  
}  

// 假设的插入数据项到缓冲区的函数(需具体实现)  
void insert_item(int item) {  
  // 这里应实现将数据项放入缓冲区的逻辑  
  // 注意:实际实现中可能需要考虑缓冲区同步和互斥访问  
}  

// 假设的从缓冲区移除数据项的函数(需具体实现)  
int remove_item(void) {  
  // 这里应实现从缓冲区取出数据项的逻辑  
  // 注意:实际实现中同样需要考虑同步和互斥  
  return 0; // 示例返回  
}  

// 模拟系统调用 sleep,这里使用忙等待(实际应使用系统提供的阻塞机制)  
void sleep(void) {  
  // 这里仅为示例,实际应使用系统提供的 sleep 函数  
  // 在这里,我们简单地让出 CPU 时间片,但这不是真正的 sleep  
  // 实际应用中,应使用如 POSIX 的 sleep() 或 Windows 的 Sleep()  
}  

// 模拟系统调用 wakeup,这里假设有某种机制可以唤醒其他线程或进程  
void wakeup(void (*func)(void)) {  
  // 这里仅为示例,实际应使用如 POSIX 的 pthread_cond_signal 或 Windows 的 SetEvent 
  // 这里我们不做任何操作,因为真正的唤醒机制依赖于操作系统  
}  

// 生产者  
void producer(void) {  
  int item;  

  // 无限循环  
  while (true) {  
    // 生成下一项数据  
    item = produce_item();  

    // 如果缓冲区是满的,就阻塞(这里用忙等待模拟)  
    while (count == N) {  
      sleep(); // 在这等着。
    }  

    // 把当前数据放在缓冲区中  
    insert_item(item);  

    // 增加缓冲区 count 的数量  
    count++;  

    // 如果缓冲区之前为空,唤醒消费者  
    if (count == 1) {  
      wakeup(consumer); // 假设可以唤醒消费者线程或进程  
    }  
  }  
}  

// 消费者  
void consumer(void) {  
  int item;  

  // 无限循环  
  while (true) {  
    // 如果缓冲区是空的,就阻塞(这里用忙等待模拟)  
    while (count == 0) {  
      sleep(); // 假设这是真正的系统调用 sleep  
    }  

    // 从缓冲区中取出一个数据  
    item = remove_item();  

    // 将缓冲区的 count 数量减一  
    count--;  

    // 如果缓冲区之前是满的,唤醒生产者  
    if (count == N - 1) {  
      wakeup(producer); // 假设可以唤醒生产者线程或进程  
    }  

    // 模拟消费数  
    consumer_item(item);  
  }  
}  

// 注意:上述代码中的 sleep 和 wakeup 函数仅为示例,实际实现应依赖具体的操作系统提供的机制。 
// 同时,缓冲区的管理(insert_item 和 remove_item)也需要实现具体的同步和互斥逻辑,以避免数据竞争。

它的问题不仅是 count++count-- 可能发生竞态,更致命的是唤醒丢失(lost wakeup)。考虑缓冲区为空时的如下交错:

  1. 消费者读取到 count == 0,准备执行 sleep()
  2. 消费者恰好被切走,还没有真正睡眠;
  3. 生产者放入一个数据项,把 count 改为 1,并执行 wakeup(consumer)
  4. 此刻消费者尚未在等待队列中,唤醒没有对象,信号被丢弃;(唤醒丢失了!)
  5. 消费者恢复执行,仍按照先前的判断进入睡眠。

之后如果生产者继续生产到缓冲区满,也会睡眠;消费者明明该消费却仍在睡,双方就可能永久等待。

问题的根源不是“调用 wakeup 的时机不够巧”,而是 if (count == 0)sleep() 是两步可被打断的操作。

解决方案之一: 是引入唤醒等待标志(Wakeup Flag)

  • 给线程额外加一个唤醒标志,在单生产者、单消费者下可以缓解问题;但在多生产者、多消费者场景中很快又会陷入复杂的计数和时序处理。
  • 更可靠的办法是使用把“资源计数”和“阻塞唤醒”一起定义好的同步原语:信号量

三、信号量:把资源数量与等待关系交给原子操作

1. 什么是信号量

荷兰计算机科学家 Dijkstra 提出的信号量(semaphore),可以理解为一个带等待队列的非负计数器。它不是普通整型变量,因为对它的操作必须是原子的。

信号量最核心的两个操作是:

  • P / down / wait:申请一个资源;
  • V / up / signal:归还或发布一个资源。

概念上,down(S) 的语义是:若 S 代表的资源还有余量,就消耗一个并继续;否则把当前线程加入 S 的等待队列并阻塞。up(S) 的语义是:发布一个资源;若有人等待,就唤醒一个合适的等待者。

down(S):原子地“检查资源 → 减少计数,或阻塞当前线程”
up(S)  :原子地“增加计数 → 必要时唤醒一个等待者”

重点在“原子地”。如果检查 S、修改 S 和阻塞线程可以被其他线程插入,信号量仍会重新出现唤醒丢失或计数错误。

教材中常见两种理解:

  • 整型信号量:只用一个整数描述资源数量;
  • 记录型信号量:除计数外还维护等待队列,P/V 能真正实现阻塞与唤醒。

实际操作系统中的实现属于后一类思想。内部计数的具体正负表示法因实现而异,但对使用者而言只需记住:down 拿不到资源会等待,up 会使一个等待者有机会继续。

2. 用三个信号量解决生产者—消费者

设缓冲区容量为 N,需要三个信号量:

信号量 初值 含义
empty N 尚未使用的空槽数量
full 0 已经存放数据的满槽数量
mutex 1 对缓冲区和共享状态的互斥访问权

完整的伪代码如下:

#include <stdio.h>
#include <sys/types.h>
#define FALSE	0 
#define TRUE	1

/* 缓冲区 slot 槽的数量 */ 
#define N 100
struct queue{ pid_t pid;};

// typedef struct {
// 	int value;
// 	struct queue wq;
// } semaphore;

/* 信号量类型定义,通常在实际应用中会使用更复杂的结构体来确保原子操作 */  
typedef int semaphore;
extern void down(semaphore *);
extern void up(semaphore *);

int produce_item(void) { 
  static int item = 0; 
  return ++item; 
}

void consumer_item(int item) {
  printf("Consumed: %d\n", item); 
}

void insert_item(int item) {

}

int remove_item(void) {
  return 0;
}

/* 标明缓存区有空位置的个数 */
semaphore empty = N;		// 缓存区初始化时有N个空位
/* 标明缓存区已经放入资源的数量 */
semaphore full = 0;			// 缓存区初始化时有0个可用资源
/* 控制区临界区互斥访问 */
semaphore mutex = 1;		// 表示互斥锁

// 生产者
void producer(void ) {
  int item;

  while (TRUE) {
    /* 生产一个数据项 */  
    item = produce_item();

    /* 等待缓冲区有空槽 */  
    down(&empty);  // 如果 empty 为 0(没有空位了),则生产者将阻塞,直到有消费者释放空槽 

    /* 进入临界区,保护对缓冲区的访问 */  
    down(&mutex);  // 确保没有其他生产者或消费者同时访问缓冲区  

    /* 将数据项放入缓冲区 */  
    insert_item(item);  // 假设这是将数据项放入缓冲区的函数  

    /* 离开临界区 */  
    up(&mutex);  // 释放互斥锁  

    /* 增加缓冲区满槽的数量 */  
    up(&full);   // 表示缓冲区中多了一个满槽 

  }
}

// 消费者
void consumer(void ) {
  int item;

  while (TRUE) {
    /* 等待缓冲区有数据可消费 */  
    down(&full);  // 如果 full 为 0,则消费者将阻塞,直到有生产者放入数据  

    /* 进入临界区 */  
    down(&mutex);  // 确保没有其他生产者或消费者同时访问缓冲区  

    /* 从缓冲区取出数据项 */  
    item = remove_item();  // 假设这是从缓冲区取出数据项的函数  

    /* 离开临界区 */  
    up(&mutex);  // 释放互斥锁  

    /* 增加缓冲区空槽的数量 */  
    up(&empty);  // 表示缓冲区中多了一个空槽  

    /* 处理取出的数据项 */  
    consume_item(item);  // 假设这是处理数据项的函数  

  }
}
/* 注意:  
   - down(&semaphore) 操作会检查信号量的值,如果为 0 则阻塞调用者,否则将其减 1。  
   - up(&semaphore) 操作会将信号量的值加 1,并可能唤醒一个或多个在该信号量上等待的进程。  
   - 这里假设 producer_item(), insert_item(), remove_item(), consume_item() 等函数  
     已经正确定义并实现了相应的功能。  
*/

这段代码最值得理解的是每个信号量的职责:

  • empty/full计数型同步信号量。它们回答“现在能不能生产/消费”;
  • mutex 是初值为 1 的二进制信号量。它回答“谁可以修改缓冲区”;
  • up(&full)up(&empty) 分别是对消费者、生产者的“条件已经成立”的通知。

还要注意顺序:生产者必须先 down(empty),确认自己能占用一个槽,再进入临界区;消费者必须先 down(full),确认存在可取数据。不能反过来先拿 mutex 再等待 empty/full,否则一个生产者可能拿着 mutex 等空槽,而消费者因为拿不到 mutex 无法取走数据,形成死锁。

信号量的 down/up 在内核中需要借助硬件原子指令、短临界区自旋锁或关中断等机制保证原子性;在多核系统里,单纯关闭当前 CPU 的中断不足以阻止其他 CPU 修改同一个信号量,仍需要跨 CPU 的原子读—改—写操作。

3. 信号量也能表示 I/O 完成

信号量不只适用于缓冲区。假设某个 I/O 请求对应的信号量初值为 0:发起 I/O 的进程执行 down 后阻塞;设备完成 I/O 并触发中断后,中断处理程序执行 up;等待进程便能继续。对进程而言,它只是在等待一个信号量,设备中断的细节被同步原语隐藏起来了。

四、互斥量:只为“独占”而生

1. mutex 与信号量有什么不同?

**互斥量(mutex)**只有“已锁定”和“未锁定”两种逻辑状态,专门用来保护临界区。相比计数型信号量,它表达的意图更明确:谁成功加锁,谁应当在完成后解锁。

对比项 计数信号量 互斥量
核心含义 可用资源的数量或事件次数 临界区的所有权
取值 通常可大于 1 逻辑上只有锁定/解锁
解锁/归还者 不强制必须是申请者 通常要求持锁线程解锁
常见用途 空槽数、任务数、I/O 完成事件 保护共享数据结构

可以把生产者—消费者中的 mutex 看作二进制信号量,但在真实线程程序里,若只需要互斥,优先选择互斥量;若需要表达“有多少个资源”“发生了多少次事件”,信号量更贴切。

2. Futex:无竞争走用户态,有竞争才请内核帮忙

Futex 机制

  • 为解决上述问题,Futex(快速用户空间互斥,Fast Userspace Mutexes)应运而生,它巧妙地融合了自旋锁与阻塞锁的优点。Futex允许进程在用户空间自旋一段时间(等待锁短暂释放),如果在此期间锁未释放,则进程将自动切换至阻塞状态,并通过内核机制进行高效的等待与唤醒。这种机制减少了不必要的内核上下文切换,同时避免了长时间自旋导致的CPU资源浪费。

Futex是Linux内核提供的一种高效同步机制,旨在实现类似于互斥锁的功能,同时显著减少因频繁陷入内核空间而导致的性能开销。

  1. 内核服务:这部分是futex机制在Linux内核中的实现,提供了一个高效的等待队列。当进程(或线程)尝试获取一个被其他进程持有的锁时,它可以通过futex系统调用将自己加入到等待队列中。此时,进程并不会立即陷入休眠状态,而是保持在一个“等待”状态,允许在用户空间进行自旋。如果自旋一段时间后锁仍未释放,进程才会真正陷入休眠,等待内核在锁被释放时将其唤醒。
  2. 用户空间库:为了简化futex的使用,Linux还提供了用户空间库,这些库封装了futex系统调用的细节,使开发者能够方便地在用户程序中实现同步机制。通过调用这些库函数,开发者可以轻松地创建锁、解锁以及等待锁等操作,而无需直接与系统调用接口打交道。

实际示例

  • 举个例子,假设有两个进程A和B,它们都需要访问同一个共享资源。进程A首先获得了访问权并锁定了该资源。此时,如果进程B尝试访问该资源,它将通过futex系统调用将自己加入到等待队列中。如果进程A很快完成了对资源的访问并释放了锁,进程B可能会在用户空间通过自旋快速获得锁,从而避免了内核切换的开销。如果进程A的访问时间较长,进程B则会在自旋一段时间后陷入休眠,等待内核在锁被释放时将其唤醒。这样,futex就能够在保证同步正确性的同时,最大限度地提高系统的性能。

因值得注意的是,当没有锁竞争时(即所有尝试获取锁的操作都能立即成功),futex机制能够完全在用户空间内运作,无需内核介入,从而实现了极高的效率。这种设计使得futex成为高并发环境下现代同步机制的理想选择。

从这一层看,Futex 和前面的信号量没有矛盾:信号量强调抽象语义,Futex 提供高效的底层等待/唤醒支撑。常见线程库会利用 Futex 实现互斥锁、条件变量等更高层原语。

3. Pthreads 中的互斥锁

POSIX 线程库把互斥锁抽象为 pthread_mutex_t。典型生命周期是“初始化 → 加锁/解锁 → 销毁”:

#include <pthread.h>

pthread_mutex_t mutex;

pthread_mutex_init(&mutex, NULL);

pthread_mutex_lock(&mutex);
/* 临界区:访问或修改共享数据 */
pthread_mutex_unlock(&mutex);

pthread_mutex_destroy(&mutex);

常用函数的含义如下:

  • pthread_mutex_lock:锁空闲则立即获得;锁被占用则阻塞,直到有机会获得锁;
  • pthread_mutex_trylock:尝试获得锁;失败立即返回,例如 EBUSY,不会阻塞;
  • pthread_mutex_unlock:由持锁线程释放锁,并使等待线程重新有机会竞争;
  • pthread_mutex_init/destroy:初始化和销毁动态初始化的互斥锁。

互斥锁只能解决“不要同时修改共享数据”,却不能直接表达“队列为空时消费者应该睡”“队列已满时生产者应该睡”(用于实现线程间的同步)。后者需要条件变量(condition variable)

4. 条件变量:等待的不是锁,而是条件

Pthreads 条件变量类型为 pthread_cond_t。它总要和一个互斥锁共同使用:互斥锁保护共享状态,条件变量让线程在某个状态不成立时等待。

最重要的调用是:

pthread_cond_wait(&cond, &mutex);

它看似只是在等待 cond,实际上做了一个不可分割的动作:

  1. 当前线程已经持有 mutex
  2. pthread_cond_wait 把线程加入 cond 的等待队列,并原子地释放 mutex
  3. 线程被唤醒后,不会立刻继续执行,而是先重新获得 mutex
  4. 重新获得锁后,函数才返回。

这正是它能避免“检查条件后再睡眠”出现丢失唤醒的原因。

条件变量还有两个常用通知函数:

pthread_cond_signal(&cond);      // 至少唤醒一个等待者
pthread_cond_broadcast(&cond);   // 唤醒所有等待者

被唤醒不等于条件一定为真。可能有多个等待者被唤醒,也可能在当前线程重新抢到互斥锁前,其他线程已经改变了共享状态;此外,条件变量允许虚假唤醒(spurious wakeup)。因此必须用 while 而不能用 if

pthread_mutex_lock(&mutex);
while (!condition) {
    pthread_cond_wait(&cond, &mutex);
}
/* 此时持有 mutex,且 condition 已被重新确认 */
pthread_mutex_unlock(&mutex);

5. 用 Pthreads 再写一次生产者—消费者

下面使用环形缓冲区、一个互斥锁和两个条件变量。not_full 表示“缓冲区尚未满”,not_empty 表示“缓冲区并非空”。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

typedef struct _node
{
  int number;
  struct _node *next;
} Node;

// 定义互斥锁与条件变量
pthread_mutex_t mutrx;
pthread_cond_t cond;

// 定义一个头插链表
Node *head = NULL;

void *th_do_fun(void *arg)
{
  // 生产者
  while (1)
  {
    pthread_mutex_lock(&mutrx);
    Node *p = (Node *)malloc(sizeof(Node));
    p->number = rand() % 100;
    p->next = head;
    head = p;
    printf("[%ld]: do [%d] OK!\n", pthread_self(), head->number);
    pthread_mutex_unlock(&mutrx);
    pthread_cond_broadcast(&cond); // pthread_cond_signal(&cond); (也不是不行)
    sleep(rand() % 3);
  }

  return NULL;
}

void *th_fk_fun(void *arg)
{
  while (1)
  {
    pthread_mutex_lock(&mutrx);
    // if(head == NULL)   // 这样写有bug
    /*
为什么在上面使用if 有bug:
    当任务队列为空, 所有的消费者线程都会被这个函数阻塞 pthread_cond_wait(&cond, &mutex);
    也就是阻塞在代码的第9行

    当生产者生产了1个节点, 调用 pthread_cond_broadcast(&cond); 唤醒了所有阻塞的线程
      - 有一个消费者线程通过 pthread_cond_wait()加锁成功, 其余没有加锁成功的线程继续阻塞
      - 加锁成功的线程向下运行, 并成功删除一个节点, 然后解锁
      - 没有加锁成功的线程解除阻塞继续抢这把锁, 另外一个子线程加锁成功
      - 但是这个线程删除链表节点的时候链表已经为空了, 后边访问这个空节点的时候就会出现段错误
    解决方案:
      - 需要循环的对链表是否为空进行判断, 需要将if 该成 while
*/
    while (!head)
    {
      // 条件变量: 阻塞线程 (因为没有了, 需要生产者生产!)
      printf("阻塞(len == 0)\n");
      // 任务队列, 也就是链表中已经没有节点可以消费了
      // 消费者线程需要阻塞
      // 线程加互斥锁成功, 但是线程阻塞在这行代码上, 锁还没解开
      // 其他线程在访问这把锁的时候也会阻塞, 生产者也会阻塞 ==> 死锁
      // 这函数会自动将线程拥有的锁解开
      pthread_cond_wait(&cond, &mutrx);
      // 当消费者线程解除阻塞之后, 会自动将这把锁锁上
      // 这时候当前这个线程又重新拥有了这把互斥锁
    }
    Node *p = head;
    head = head->next;
    printf("[%ld]: fk [%d] OK!\n", pthread_self(), p->number);
    free(p);
    pthread_mutex_unlock(&mutrx);
    sleep(rand() % 2);
  }

  return NULL;
}

int main()
{
  // 初始化
  pthread_mutex_init(&mutrx, NULL);
  pthread_cond_init(&cond, NULL);

  // 定义生产者(do)与消费者(fk)线程
  pthread_t do_arr[5], fk_arr[5];
  for (int i = 0; i < 5; ++i)
  {
    pthread_create(&do_arr[i], NULL, th_do_fun, NULL);
    printf("The do Man (%ld) Go !\n", do_arr[i]);
  }

  for (int i = 0; i < 5; ++i)
  {
    pthread_create(&fk_arr[i], NULL, th_fk_fun, NULL);
    printf("The fk Man (%ld) Go !\n", fk_arr[i]);
  }

  // 释放
  while (head)
  {
    Node *tmp = head;
    head = head->next;
    free(tmp);
  }

  for (int i = 0; i < 5; ++i)
  {
    pthread_join(do_arr[i], NULL);
    pthread_join(fk_arr[i], NULL);
  }

  pthread_mutex_destroy(&mutrx);
  pthread_cond_destroy(&cond);
  return 0;
}

这里和信号量版本形成了清晰对应:

  • mutex 仍负责保护 bufferinoutcount
  • while (count == N) 对应“没有空槽就不能生产”;
  • while (count == 0) 对应“没有数据就不能消费”;
  • not_full/not_empty 不保存资源数量,它们只负责让等待者在条件变化时重新检查状态;
  • count 才是条件的真实依据,所以读它、改它和检查它都必须在同一把 mutex 的保护下完成。

如果有多个生产者或消费者,使用 signal 通常足够;在一次状态变化可能让多个等待者都重新具备运行条件,或实现上难以精确判断唤醒对象时,也可使用 broadcast。无论哪一种,等待端的 while 都不能省略。

五、总结:先分清“独占”还是“等条件”

从生产者—消费者问题可以看到,并发控制不是只加一把锁:

  • 共享缓冲区的读写需要互斥
  • 空与满的边界需要同步
  • sleep/wakeup 若没有原子地协调检查与等待,就会产生唤醒丢失;
  • 信号量用 emptyfullmutex 同时表达资源数量、先后关系和临界区保护;
  • 互斥锁专注于所有权;条件变量专注于“条件未满足就等待”;
  • Futex 则体现了现代实现的取舍:无竞争时尽量停留在用户态,真正需要等待时再交给内核排队与唤醒。

面对一个并发问题,可以按下面的顺序判断:先找出共享数据与临界区;再问当前需要的是“只能一个人做”的互斥,还是“必须等某件事发生”的同步;最后选择合适的原语,并确认所有“检查—修改—等待/唤醒”的关键步骤没有可被插入的竞态窗口。


如果这篇文章对你有帮助,欢迎点赞、评论、关注、收藏。你们的支持是我前进的动力!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐