【组件-池式】线程池2-线程互斥

声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。

摘要

总结整理 POSIX 和 C++ 提供的线程互斥技术,包括互斥锁、读写锁、信号量、自旋锁以及原子变量,介绍相关概念、接口和使用示例。另外还包括几点死锁预防建议。


通过引入多线程,可以提高程序的执行效率。

  • 在理想情况下,每个线程可以独立工作,无所顾忌,全速运行;
  • 在现实情况下,线程之间通常需要分工协作,需要涉及
    • 线程互斥:对共享资源进行保护,线程之间按照一定的规则有序访问;
    • 线程同步:在一个线程完成特定的工作之后,另一个线程才能够继续执行。

本章主要总结线程互斥相关技术。

1 基本概念

1.1 资源共享问题

多个线程并发访问同一份共享资源时,如果一个线程正在进行修改,同时其他线程正在进行读取或者修改,它们之间就会产生冲突,引发数据竞争(data race)。

数据竞争示例:

// 编译指令:g++ -pthread test.cpp
#include <pthread.h>
#include <stdio.h>

#define LOOPS 100000000  // 循环次数
int gcn = 0;             // 全局计数变量,初始为 0

void* thread_proc1(void* arg) {
  for (int i = 0; i < LOOPS; i++) {
    gcn += 1;  // 一个线程对 gcn 进行 LOOPS 次 +1
  }
  return NULL;
}

void* thread_proc2(void* arg) {
  for (int i = 0; i < LOOPS; i++) {
    gcn -= 1;  // 一个线程对 gcn 进行 LOOPS 次 -1
  }
  return NULL;
}

int main() {
  pthread_t pid1, pid2;
  for (int i = 0; i < 10; i++) {  // 测试 10 次
    pthread_create(&pid1, NULL, thread_proc1, NULL);
    pthread_create(&pid2, NULL, thread_proc2, NULL);
    pthread_join(pid1, NULL);
    pthread_join(pid2, NULL);
    printf("gcn %d: %d\n", i, gcn);  // 查看 gcn 是否为 0
    gcn = 0;
  }
}

结果示意:gcn 的多次输出中,存在部分输出不是 0

gcn 0: 5650432
gcn 1: 0
gcn 2: 3087238
gcn 3: 0
gcn 4: 563284
gcn 5: -2728073
gcn 6: 0
gcn 7: 0
gcn 8: 0
gcn 9: -6779036

问题分析: 语句 gcn += 1;gcn -= 1; 通常会被编译成 3 条机器指令:

  1. 读:将变量 gcn 对应的内存值读入到某个寄存器 reg 中;
  2. 改:将寄存器 reg 中的值加 1 或减 1
  3. 写:将寄存器 reg 中的值写回 gcn 对应的内存单元。

示例中的两个线程,并发 +1-1 的执行顺序可能如下:

步骤线程1线程2gcnreg
1将内存 gcn 中的值读入寄存器 reg假设为00
2将寄存器 reg 中的值加 1
(线程挂起,保存 reg
01
3将内存 gcn 中的值读入寄存器 reg00
4将寄存器 reg 中的值减 10-1
5将寄存器 reg 中的值写回内存 gcn-1-1
6恢复寄存器 reg,将值写回内存 gcn11

上述示例为“一条语句的执行被其他线程打断”的情况,对于由多条语句组成的任务,也会存在被其他线程打断的情况。

1.2 资源互斥访问

对于线程之间因共享数据而引发的数据竞争,可以通过互斥机制来保证线程之间的有序执行。

基本概念:

  • 临界资源:一次仅允许一个线程使用的共享资源;
  • 临界区:每个线程中访问临界资源的那段代码。

典型的互斥访问:

  1. 如果有若干线程要求进入空闲的临界区,一次仅允许一个线程进入;
  2. 如果已有线程进入临界区,那么其他所有试图进入临界区的线程需要等待(阻塞,让出 CPU);
  3. 进入临界区的线程要在有限时间内退出,以便其他线程能够及时进入临界区。

互斥机制有多种实现方式,包括:

  • 互斥锁:一次仅允许一个线程进入临界区(典型的互斥访问);
  • 读写锁:允许读线程同时进入临界区;
  • 信号量:允许一定数量的线程同时进入临界区;
  • 自旋锁:一次仅允许一个线程进入临界区(等待线程不阻塞);
  • 原子变量:一次仅允许一个线程访问变量(低级别、锁无关)。

2 基于 POSIX 的线程互斥

2.1 互斥锁

互斥锁(互斥体/互斥量):同一时刻,只允许一个线程对临界区进行访问。

使用流程:

  1. 初始化一个互斥锁(多个线程共用);
  2. 线程在进入临界区之前,加锁(阻止其他线程进入临界区);
  3. 线程在退出临界区之后,解锁(允许其他线程进入临界区);
  4. 销毁互斥锁。
初始化互斥锁
处理完成
销毁互斥锁
加锁
访问临界资源
解锁

2.1.1 常用 API 函数

互斥锁 mutex(mutual exclusion,相互排斥)。

序号函数说明
1int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);按照指定属性(NULL 为默认属性)初始化互斥锁。
2int pthread_mutex_destroy(pthread_mutex_t *mutex);销毁互斥锁(不能正在被使用,如:已加锁 / 其它线程正尝试加锁 / 被 pthread_cond_wait() 等函数使用)。
3pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;以静态方式初始化互斥锁(按照默认属性,使用 PTHREAD_MUTEX_INITIALIZER 直接给互斥锁变量赋值),不需要销毁。
4int pthread_mutex_lock(pthread_mutex_t *mutex);对互斥锁加锁,如果互斥锁已经被其他线程加锁,那么调用该函数的线程将阻塞(让出 CPU)
5int pthread_mutex_trylock(pthread_mutex_t *mutex);pthread_mutex_lock,除外:如果互斥锁已经被任意线程加锁,则函数立即返回 EBUSY(对于默认属性)
6int pthread_mutex_timedlock(pthread_mutex_t* restrict mutex, const struct timespec* restrict abstime);pthread_mutex_lock,除外:如果互斥锁已经被任意线程加锁,则进行等待,直到超时返回 ETIMEDOUT(对于默认属性)
7int pthread_mutex_unlock(pthread_mutex_t *mutex);对互斥锁进行解锁。pthread_mutex_unlock 要和 pthread_mutex_*lock 成对使用,避免死锁。

关于返回值:

  • 如果成功,函数均返回 0
  • 2.1.2 设置类型属性 部分,对于不同类型属性,加锁/解锁后,会有不同的状态及返回值;
  • 在内存等资源不足、权限不足、参数错误、加锁数量超出上限等情况下,会返回相应错误码。

通过 man7.org/linux/man-pages 搜索 pthread_mutex 可以查看更多相关函数,以及更详细的说明。

2.1.2 使用示例

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

#define LOOPS 10000000  // 循环次数
int gcn = 0;            // 全局计数变量(由互斥锁保护)
pthread_mutex_t mutex;  // 全局互斥锁

void* thread_proc1(void* arg) {
  for (int i = 0; i < LOOPS; i++) {
    pthread_mutex_lock(&mutex);    // 加锁
    gcn += 1;                      // 包含临界资源 gcn 的临界区
    pthread_mutex_unlock(&mutex);  // 解锁
  }
  return NULL;
}

void* thread_proc2(void* arg) {
  for (int i = 0; i < LOOPS; i++) {
    pthread_mutex_lock(&mutex);
    gcn -= 1;
    pthread_mutex_unlock(&mutex);
  }
  return NULL;
}

int main() {
  pthread_t pid1, pid2;
  pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁
  for (int i = 0; i < 10; i++) {     // 测试 10 次
    pthread_create(&pid1, NULL, thread_proc1, NULL);
    pthread_create(&pid2, NULL, thread_proc2, NULL);
    pthread_join(pid1, NULL);
    pthread_join(pid2, NULL);
    printf("gcn %d: %d\n", i, gcn);  // gcn 值均为 0
    gcn = 0;
  }
  pthread_mutex_destroy(&mutex);  // 销毁互斥锁
}

2.1.3 设置类型属性

对于分时调度(默认调度策略)线程,互斥锁属性包括:进程共享属性(pshared)、健壮属性(robust,线程未解锁退出情况)、类型属性(type)。
其中,类型属性相关 API 函数如下:

序号函数说明
1int pthread_mutexattr_init(pthread_mutexattr_t *attr);初始化互斥锁属性结构。
2int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);销毁互斥锁属性结构。(在完成互斥锁初始化后,就可以将其销毁)
3int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);设置互斥锁类型属性,包括 PTHREAD_MUTEX_NORMAL(默认)、PTHREAD_MUTEX_ERRORCHECKPTHREAD_MUTEX_RECURSIVE
4int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);获取互斥锁类型属性。

对于不同的锁类型,重复加锁(Relock)及非法解锁(Unlock When Not Owner)的行为如下:

锁类型类型标识加锁函数已加锁后再加锁
(线程间)
已加锁后再加锁
(线程内)
已加锁后解锁
(线程内)
未加锁时解锁
(线程内)
普通锁PTHREAD_MUTEX_NORMALpthread_mutex_lock()阻塞等待死锁其他线程可加锁未定义行为
pthread_mutex_trylock()返回 EBUSY返回 EBUSY
pthread_mutex_timedlock()返回 ETIMEDOUT返回 ETIMEDOUT
检错锁PTHREAD_MUTEX_ERRORCHECKpthread_mutex_lock()NORMAL返回 EDEADLK返回 EPERM
pthread_mutex_trylock()NORMAL返回 EBUSY
pthread_mutex_timedlock()NORMAL返回 EDEADLK
递归锁PTHREAD_MUTEX_RECURSIVEpthread_mutex_*lock()NORMAL增加锁计数减少锁计数,计数为 0 时,其他线程可加锁返回 EPERM
默认锁PTHREAD_MUTEX_DEFAULT
在 linux 中同 NORMAL
/////

对于不同类型属性,测试加锁后的返回值:

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

#define handle_error_en(en, msg) \
  do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

pthread_mutex_t mutex;  // 全局互斥锁

void* thread_proc(void* arg) {
  for (int i = 3; i > 0; i--) {
    pthread_mutex_lock(&mutex);
    printf("thread_proc [%d]\n", i);
    sleep(3);
    pthread_mutex_unlock(&mutex);
  }
  return NULL;
}

int main() {
  int ret;
  pthread_t pid;

  pthread_mutexattr_t attr;
  pthread_mutexattr_init(&attr);                                           // 属性初始化
  if (ret = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK)) {  // 赋值类型属性
    handle_error_en(ret, "pthread_mutexattr_settype");
  }
  pthread_mutex_init(&mutex, &attr);  // 为 mutex 设置属性

  int type;
  pthread_mutexattr_gettype(&attr, &type);  // 获取当前类型属性
  printf("Attr Type = %s\n", (type == PTHREAD_MUTEX_NORMAL)       ? "MUTEX_NORMAL"
                             : (type == PTHREAD_MUTEX_ERRORCHECK) ? "MUTEX_ERRORCHECK"
                             : (type == PTHREAD_MUTEX_RECURSIVE)  ? "MUTEX_RECURSIVE"
                                                                  : "???");
  pthread_mutexattr_destroy(&attr);  // 属性销毁

  if (ret = pthread_create(&pid, NULL, thread_proc, NULL)) handle_error_en(ret, "pthread_create");
  sleep(1);  // 让子线程先执行

  int locked = 0;  // 加锁计数
  if ((ret = pthread_mutex_trylock(&mutex)) == 0) locked++;  // trylock()
  printf("trylock ret(%d) vs EBUSY(%d)\n", ret, EBUSY);

  struct timespec time_out;
  clock_gettime(CLOCK_REALTIME, &time_out);
  time_out.tv_sec += 1;
  if ((ret = pthread_mutex_timedlock(&mutex, &time_out)) == 0) locked++;  // timedlock()
  printf("timedlock ret(%d) vs ETIMEDOUT(%d)\n", ret, ETIMEDOUT);

  if ((ret = pthread_mutex_lock(&mutex)) == 0) locked++;  // lock()
  printf("lock ret(%d) vs EDEADLK(%d)\n", ret, EDEADLK);

  if (type != PTHREAD_MUTEX_NORMAL) {
    if ((ret = pthread_mutex_lock(&mutex)) == 0) locked++;  // lock()
    printf("relock ret(%d) vs EDEADLK(%d)\n", ret, EDEADLK);
  }

  sleep(1);

  for (; locked > 0; locked--) {
    ret = pthread_mutex_unlock(&mutex);  // unlock()
    printf("unlock [%d] ret(%d)\n", locked, ret);
  }

  pthread_join(pid, NULL);
  pthread_mutex_destroy(&mutex);
}

2.2 读写锁

对共享数据的访问操作,可以分为“读操作”和“写操作”。对于读操作,即便有多个线程并发读取,也不会像写操作那样造成数据竞争。
利用上述特性,在读操作远多于写操作的场景中,使用“读写锁”可以达到比“互斥锁”更好的并发性能:

  1. 如果一个线程用读锁锁住了临界区,那么其他线程也可以用读锁来进入临界区,从而实现并发读取;
    • 加读锁后,如果其他线程再请求加写锁,就会发生阻塞;
    • 在写锁请求阻塞后,如果其他线程继续有读锁请求,那么这些读锁请求是否会阻塞,需要根据锁的属性判断,应尽量避免“读锁长期占用资源、写锁饥饿”的情况。
  2. 如果一个线程用写锁锁住了临界区,那么其他线程不管是请求读锁还是写锁都会发生阻塞。

2.2.1 常用 API 函数

序号函数说明
1int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);按照指定属性(NULL 为默认属性)初始化读写锁。
2int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);销毁读写锁(不能正在被使用,如:已加锁 / 其它线程正尝试加锁)
3pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;以静态方式初始化读写锁(按照默认属性,使用 PTHREAD_RWLOCK_INITIALIZER 直接给读写锁变量赋值),不需要销毁。
4int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);为读操作加锁(加读锁),如果读写锁已经被其他线程加写锁,那么调用该函数的线程将阻塞;否则,如果某个线程正在等待加写锁(已加读锁),函数是否阻塞由实现定义(对于分时调度策略)。
在同一个线程内,可以多次加读锁(默认属性下),需要确保同样次数的解锁。
如果当前线程已经加写锁,再次加读锁,将返回 EDEADLK,或者产生死锁
5int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);为写操作加锁(加写锁),如果读写锁已经被其他线程加锁(无论是读锁还是写锁),那么调用该函数的线程将阻塞。
如果检测到死锁条件,或者在当前线程已经加读锁或写锁的情况下,再次加写锁,将返回 EDEADLK,或者产生死锁
6int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);pthread_rwlock_rdlock(),除外:对于 pthread_rwlock_rdlock() 需要阻塞的情况(其他线程已经加写锁或正在等待加写锁),或者当前线程已经加写锁,函数立即返回 EBUSY
7int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);pthread_rwlock_wrlock(),除外:如果任意线程已经加读锁或写锁,函数立即返回 EBUSY
8int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abstime);pthread_rwlock_rdlock(),除外:对于 pthread_rwlock_rdlock() 需要阻塞的情况(其他线程已经加写锁或正在等待加写锁),则进行等待,直到超时返回 ETIMEDOUT
如果当前线程已经加写锁,或者检测到死锁条件,则返回 EDEADLK
9int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abstime);pthread_rwlock_wrlock(),除外:如果其他线程已经加读锁或写锁,则进行等待,直到超时返回 ETIMEDOUT
如果当前线程已经加读锁或写锁,或者检测到死锁条件,将返回 EDEADLKETIMEDOUT(已加读锁?)
10int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);对读写锁进行解锁,需要与加锁(读锁及写锁)函数成对使用,避免死锁。

如果成功,函数均返回 0,否则返回错误码(TODO:待进步梳理)。
通过 man7.org/linux/man-pages 搜索 pthread_rwlock 可以查看更多相关函数,以及更详细的说明。

对于“分时调度”线程,pthread_rwlock_rdlock()pthread_rwlock_wrlock() 在“默认属性”下的加锁情况:

当前锁状态请求加读锁
(线程间)
请求加读锁
(线程内)
请求加写锁
(线程间)
请求加写锁
(线程内)
无锁成功加锁成功加锁成功加锁成功加锁
已经加读锁
(无写锁阻塞)
成功加锁成功加锁阻塞死锁
(经测试)
已经加读锁
(有写锁阻塞)
实现定义
(经测试可加)
实现定义
(经测试可加)
阻塞死锁
(经测试)
已经加写锁阻塞返回 EDEADLK
(经测试)
阻塞返回 EDEADLK
(经测试)

2.2.2 设置类型属性

读写锁属性包括:进程共享属性(pshared)、类型属性(kind)。
其中,类型属性相关 API 函数如下:

序号函数说明
1int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);初始化读写锁属性结构。
2int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);销毁读写锁属性结构。(在完成读写锁初始化后,就可以将其销毁)
3int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);设置读写锁类型属性,包括 PTHREAD_RWLOCK_PREFER_READER_NP(默认)、PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP
4int pthread_rwlockattr_getkind_np(const pthread_rwlockattr_t *restrict attr, int *restrict pref);获取读写锁类型属性。

名称中的后缀 “_np” 代表 “nonportable”(不可移植)。

类型说明:

序号类型说明
1PTHREAD_RWLOCK_PREFER_READER_NP读者优先:如果已加读锁,即使其他线程存在阻塞等待的写锁,接下来的读锁请求仍将成功加锁。
也就是,只要有读锁存在,就会导致写锁饥饿(无法成功加锁)。
一个线程可以持有多个读锁,即线程内部读锁是可递归的。
2PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP写者优先:如果已加读锁,并且其他线程存在阻塞等待的写锁,接下来的读锁请求将会阻塞,避免写锁饥饿。
同时,通过“NONRECURSIVE”标明,在线程内部,不可以递归请求读锁,如果已经持有读锁,并且其他线程存在阻塞等待的写锁,再次尝试请求读锁将会死锁(经测试)。

2.2.3 读写锁优先级测试

#include <errno.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define handle_error(msg) \
  do { perror(msg); exit(EXIT_FAILURE); } while (0)

#define handle_error_en(en, msg) \
  do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

#define READER_COUNT 5  // 读线程数量 vs 1个写线程
#define LOOPS 5         // 线程内部加/解锁次数

int gcn = 0;              // 全局变量,读多写少,通过读写锁保护
pthread_rwlock_t rwlock;  // 全局读写锁

/**
 * 加锁时空示意
 * 读线程1:读——|——读——|——读——|——读——|——读
 * 读线程2:——读——|——读——|——读——|——读——|——读
 * 读线程3:——|——读——|——读——|——读——|——读——|——读
 * 读线程4:——|——|——读——|——读——|——读——|——读——|——读
 * 读线程5:——|——|——|——读——|——读——|——读——|——读——|——读
 * 写线程1:——|——写?
 */

void *thread_read(void *arg) {
  int idx = (int)(long)(arg);  // 读线程编号,从 0 开始
  sleep(idx);  // 每隔 1 秒开始一个读锁请求,确保写锁请求位于中间,以便观察写锁请求的优先级

  printf("reader %d: begin\n", idx);
  for (int i = LOOPS; i > 0; i--) {
    pthread_rwlock_rdlock(&rwlock);  // 加读锁
    printf("reader %d: loop [%d] reading (%d)...\n", idx, i, gcn);
    sleep(2);  // 读线程睡眠 2 秒,确保持续存在读锁请求,以便观察写锁请求的优先级
    pthread_rwlock_unlock(&rwlock);  // 释放读锁
  }
  printf("reader %d: end\n", idx);
  return NULL;
}

void *thread_write(void *arg) {
  sleep(1);  // 确保写锁请求在第 1 个读锁请求之后,以便观察写锁请求的优先级

  printf("writer    begin\n");
  for (int i = LOOPS; i > 0; i--) {
    pthread_rwlock_wrlock(&rwlock);  // 加写锁
    printf("writer    loop [%d] writing (%d)...\n", i, ++gcn);
    sleep(1);  // 写线程睡眠 1 秒,增加“写锁请求”的频率
    pthread_rwlock_unlock(&rwlock);  // 释放写锁
  }
  printf("writer    end\n");
  return NULL;
}

int main() {
  int ret;
  pthread_t pids[READER_COUNT];
  pthread_t wid;

#if 0 // 默认属性
  if (ret = pthread_rwlock_init(&rwlock, NULL)) handle_error_en(ret, "pthread_rwlock_init");
#else
  pthread_rwlockattr_t attr;
  if (ret = pthread_rwlockattr_init(&attr)) handle_error_en(ret, "pthread_rwlockattr_init");
  if (ret = pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_READER_NP)) handle_error_en(ret, "pthread_rwlockattr_setkind_np");

  if (ret = pthread_rwlock_init(&rwlock, &attr)) handle_error_en(ret, "pthread_rwlock_init");

  int kind;
  pthread_rwlockattr_getkind_np(&attr, &kind);
  printf("Attr Kind = %s\n", (kind == PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP) ? "PREFER_WRITER_NONRECURSIVE_NP"
                             : (kind == PTHREAD_RWLOCK_PREFER_READER_NP)            ? "PREFER_READER_NP(default)"
                             : (kind == PTHREAD_RWLOCK_PREFER_WRITER_NP)            ? "PREFER_WRITER_NP(ignored by glibc)"
                                                                                    : "???");

  if (ret = pthread_rwlockattr_destroy(&attr)) handle_error_en(ret, "pthread_rwlockattr_destroy");
#endif

  if (ret = pthread_create(&wid, NULL, thread_write, NULL)) handle_error_en(ret, "pthread_create");
  for (int i = 0; i < READER_COUNT; i++) {
    if (ret = pthread_create(&pids[i], NULL, thread_read, (void *)(long)i)) handle_error_en(ret, "pthread_create");
  }

  if (ret = pthread_join(wid, NULL)) handle_error_en(ret, "pthread_join");
  for (int i = 0; i < READER_COUNT; i++) {
    if (ret = pthread_join(pids[i], NULL)) handle_error_en(ret, "pthread_join");
  }

  if (ret = pthread_rwlock_destroy(&rwlock)) handle_error_en(ret, "pthread_rwlock_destroy");
}

2.3 信号量

信号量是一种特殊类型的“变量”:

  • 初始化为非负整数值(代表可共享资源的数量),可以被增加(生产/解锁)或 减少(消费/加锁);
  • 对信号量的访问是原子操作,也就是,如果有两个(或更多)的线程试图改变一个信号量的值,系统将保证所有的操作都会依次(串行)进行。

根据信号量的取值可以分为:

  1. 二值信号量(binary semaphore):只有 01 两种取值(一个可用资源),每次只允许一个线程进入临界区;
    • 当临界区可用时,信号量的值是 1
    • 在线程进入临界区前,将信号量的值减 1 变为 0(加锁),表示临界区正在被使用,此时其它需要进入临界区的线程只能等待;
    • 在线程离开临界区后,将信号量的值加 1 变为 1(解锁),使临界区再次变为可用,此时等待进入临界区的某个线程将被唤醒。
  2. 计数信号量(counting semaphore):可以有更大的取值范围(多个可用资源),允许有限数量的线程同时进入临界区。
    • 在线程进入临界区前,减少信号量的值,在减之前,如果值不大于 0(没有可用资源),线程将阻塞等待;
    • 在线程离开临界区后,增加信号量的值,在加之后,如果存在阻塞等待线程,则根据调度策略将某个线程唤醒。

上述功能与互斥锁类似,不同的是:

  • 信号量允许多个线程同时进入临界区(有多个资源可供同时访问,如多台打印机);
    而互斥锁一次只允许一个线程进入临界区。
  • 信号量可以在一个线程中增加值(生产/解锁),在另一个线程中减少值(消费/加锁),这使其可以用于线程间的同步(“线程同步”相关内容会在下一章展开);
    而互斥锁要求加锁和解锁要在同一线程内进行。

2.3.1 常用 API 函数

序号函数说明
1int sem_init(sem_t *sem, int pshared, unsigned value);初始化由 sem 指向的信号量对象,并给它一个初始的整数值 value。共享选项 pshared 如果为 0,表示当前进程内的局部信号量;否则,这个信号量可以在多个进程之间共享。
2int sem_destroy(sem_t *sem);销毁由 sem 指向的信号量对象(不能正在被使用)。
3int sem_post(sem_t *sem);以原子操作的方式给信号量的值加 1。在加之后,如果存在阻塞在 sem_wait() 的线程,则根据调度策略将某个线程唤醒。
4int sem_wait(sem_t *sem);以原子操作的方式将信号量的值减 1。在减之前,如果信号量的值大于 0,则函数执行减法后立即返回;否则函数会阻塞等待,直到有其他线程增加了该信号量的值使其可以进行减操作,或者函数调用被 signal 中断(errno 赋值 EINTR)。
5int sem_trywait(sem_t *sem);sem_wait。除外:如果不能立即减 1,函数直接返回 -1errno 赋值 EAGAIN),而不是阻塞。
6int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict abs_timeout);sem_wait。除外:如果不能立即减 1,则进行等待,直到超时返回 -1errno 赋值 ETIMEDOUT)。

如果成功,函数均返回 0,否则返回 -1,同时设置 errno

通过 man7.org/linux/man-pages 搜索 sem_ 可以查看更多相关函数,以及更详细的说明。

2.3.2 互斥示例

#include <errno.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define handle_error(msg) \
  do { perror(msg); exit(EXIT_FAILURE); } while (0)

#define handle_error_en(en, msg) \
  do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

#define PID_COUNT 5  // 消费者线程数量
#define RES_COUNT 3  // 资源数量

sem_t sem;  // 定义全局信号量

void *thread_proc(void *arg) {
  int idx = (int)(long)(arg);
  printf("sub thread %d begin\n", idx);

  while (sem_wait(&sem)) {                        // 信号量值减 1(加锁)
    if (errno == EINTR) continue;                 // Restart if interrupted by a signal handler.
    handle_error("sem_wait");
  }

  printf("sub thread %d processing...\n", idx);
  sleep(idx % 2 + 1);
  if (sem_post(&sem)) handle_error("sem_post");  // 信号量值加 1(解锁)

  printf("sub thread %d end\n", idx);
  return NULL;
}

int main() {
  int ret;
  pthread_t pids[PID_COUNT];

  // 初始化信号量,值为 RES_COUNT(可同时访问资源数量)
  if (sem_init(&sem, 0, RES_COUNT)) handle_error("sem_init");

  for (int i = 0; i < PID_COUNT; i++) {
    if (ret = pthread_create(&pids[i], NULL, thread_proc, (void *)(long)i)) handle_error_en(ret, "pthread_create");
  }

  for (int i = 0; i < PID_COUNT; i++) {
    if (ret = pthread_join(pids[i], NULL)) handle_error_en(ret, "pthread_join");
  }

  if (sem_destroy(&sem)) handle_error("sem_destroy");
}

2.3.3 同步示例

#include <errno.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define handle_error(msg) \
  do { perror(msg); exit(EXIT_FAILURE); } while (0)

#define handle_error_en(en, msg) \
  do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

sem_t sem;  // 定义全局信号量

#define BUF_SIZE 1024
char buf[BUF_SIZE];  // 生产者线程向 buf 写入数据,消费者线程从 buf 读取数据

void *thread_proc(void *arg) {
  if (sem_wait(&sem)) handle_error("sem_wait");  // 消费者线程(将信号量的值减 1),等待生产者线程向 buf 写入数据
  while (strcmp(buf, "q\n")) {                   // 读取 buf 数据
    printf("Input %ld characters\n", strlen(buf) - 1);
    if (sem_wait(&sem)) handle_error("sem_wait");  // 继续等待 buf 写入新的数据
  }
  return NULL;
}

int main() {
  int ret;
  pthread_t pid;

  // 初始化信号量,值为 0(即,buf 中没有可供消费的数据,消费者线程会在 sem_wait() 处阻塞)
  if (sem_init(&sem, 0, 0)) handle_error("sem_init");
  if (ret = pthread_create(&pid, NULL, thread_proc, NULL)) handle_error_en(ret, "pthread_create");

  printf("Input some text. Enter 'q' to quit\n");
  while (strcmp(buf, "q\n")) {
    fgets(buf, BUF_SIZE, stdin);
    // 生产者线程,在向 buf 写入数据后,将信号量的值加 1,唤醒阻塞的消费者线程(如果存在)
    if (sem_post(&sem)) handle_error("sem_post");
  }

  if (ret = pthread_join(pid, NULL)) handle_error_en(ret, "pthread_join");
  if (sem_destroy(&sem)) handle_error("sem_destroy");
}

2.4 自旋锁

自旋锁(Spinlock)是一种低级别(low-level)的互斥机制,主要适用于多处理器之间共享资源。
当线程请求一个已经被其他线程持有的锁时,这个线程会在一个循环中反复检查锁是否可用(即“自旋”),而不是进入阻塞状态。
线程在成功加锁之后,应该在尽量短的时间内解锁,以减少其他请求线程因为“自旋”造成的对处理器时间的浪费。

使用自旋锁的注意事项:

  1. 自旋锁应该在实时调度策略(如 SCHED_FIFO,或 SCHED_RR 如果可能的话)下使用。
    因为在非确定性调度策略(如 SCHED_OTHER)下,如果一个线程在持有自旋锁的情况下被调离 CPU,那么其他请求锁的线程将会一直消耗 CPU 时间在锁上自旋,直到锁的持有者再次被调度并释放锁。
  2. 在实时调度策略下,为了避免持有自旋锁的线程被抢占,高优先级的线程可能会等待低优先级线程的执行,导致发生优先级反转。
  3. 使用自旋锁时,需要特别注意避免死锁的发生。
    因为如果线程在持有自旋锁的情况下发生了死锁,那么其他请求锁的线程将会一直消耗 CPU 时间在锁上自旋。
  4. 在用户空间,不适合选择自旋锁作为一般的加锁解决方案。
    因为它们本质上容易发生优先级反转和无限自旋消耗。
  5. 使用自旋锁编写程序,不仅在代码方面,而且在系统配置、线程放置和优先级分配等方面,都必须特别小心。

2.4.1 常用 API 函数

序号函数说明
1int pthread_spin_init(pthread_spinlock_t *lock, int pshared);按照 pshared 属性(是否在进程间共享)初始化自旋锁。
2int pthread_spin_destroy(pthread_spinlock_t *lock);销毁自旋锁(不能正在被使用,如:已加锁)
3int pthread_spin_lock(pthread_spinlock_t *lock);请求加锁,如果其他线程已加锁,调用线程将会自旋(即,函数调用不会返回),直到锁变得可用;
如果在调用发生时,调用线程已经持有该锁,结果是未定义的,可能死锁,可能返回 EDEADLK
4int pthread_spin_trylock(pthread_spinlock_t *lock);pthread_spin_lock,除外:如果自旋锁已经被加锁,返回 EBUSY
5int pthread_spin_unlock(pthread_spinlock_t *lock);对自旋锁进行解锁。需要与加锁成对使用。

如果成功,函数均返回 0,否则返回错误码。
通过 man7.org/linux/man-pages 搜索 pthread_spin 可以查看更详细的说明。

2.4.2 使用示例

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

#define LOOPS 10000000    // 循环次数
int gcn = 0;              // 全局变量,通过自旋锁保护
pthread_spinlock_t lock;  // 全局自旋锁

void* thread_proc1(void* arg) {
  for (int i = 0; i < LOOPS; i++) {
    pthread_spin_lock(&lock);
    gcn += 1;
    pthread_spin_unlock(&lock);
  }
  return NULL;
}

void* thread_proc2(void* arg) {
  for (int i = 0; i < LOOPS; i++) {
    pthread_spin_lock(&lock);
    gcn -= 1;
    pthread_spin_unlock(&lock);
  }
  return NULL;
}

int main() {
  pthread_t pid1, pid2;
  pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);  // 初始化自旋锁
  for (int i = 0; i < 10; i++) {                      // 测试 10 次
    pthread_create(&pid1, NULL, thread_proc1, NULL);
    pthread_create(&pid2, NULL, thread_proc2, NULL);
    pthread_join(pid1, NULL);
    pthread_join(pid2, NULL);
    printf("gcn %d: %d\n", i, gcn);  // gcn 值均为 0
    gcn = 0;
  }
  pthread_spin_destroy(&lock);  // 销毁自旋锁
}

3 基于 C++ 的线程互斥

由操作系统提供的 API,虽然限制最少,但使用起来并不是很方便,也难以进行跨系统兼容。
从 C++11 标准开始,C++ 语言提供了一系列线程互斥与同步支持。

3.1 互斥锁(C++11)

“互斥锁概念”回顾

3.1.1 类定义

与互斥锁相关的类和函数在头文件 <mutex> 中声明:

序号说明
1mutex基本互斥锁(排他、不可递归)(对比PTHREAD_MUTEX_NORMAL
2timed_mutex定时互斥锁,可以设置加锁超时时间
3recursive_mutex递归互斥锁,在线程内可以递归加锁(对比PTHREAD_MUTEX_RECURSIVE
4recursive_timed_mutex定时递归互斥锁,结合 timed_mutexrecursive_mutex 特性

各类的成员函数:

序号成员函数mutextimed_
mutex
recursive_
mutex
recursive_
timed_mutex
说明
1构造函数提供默认构造函数;不可拷贝,不可移动。
2析构函数
3void lock();阻塞式加锁,失败抛异常
4bool try_lock();如果不能立刻加锁,返回 false
5void unlock();解锁,需要与加锁成对使用,避免死锁
6native_handle_type native_handle();Linux 对应 pthread_mutex_t*
7template<class Rep, class Period>
bool try_lock_for(const chrono::duration<Rep, Period>& timeout_duration);
如果等待超过指定时长,返回 false
8template<class Clock, class Duration>
bool try_lock_until(const chrono::time_point<Clock, Duration>& timeout_time);
如果等待到达指定时间点,返回 false

虽然,可以直接调用成员函数 lock() 进行加锁、unlock() 进行解锁。
但是,为了让 unlock() 始终与 lock() 成对调用,推荐使用标准库提供的、融合实现 RAII 的类模板,如 std::lock_guard<>

// 头文件 include/c++/10/bits/std_mutex.h

// 用于指定“锁定策略”的标记类型和常量,表示当前线程已经成功加锁
struct adopt_lock_t { explicit adopt_lock_t() = default; };
_GLIBCXX17_INLINE constexpr adopt_lock_t adopt_lock{};

template <typename _Mutex>
class lock_guard {  // 基于作用域的互斥锁包装器
 public:
  typedef _Mutex mutex_type;  // 互斥锁类型

  // 在构造函数中,通过互斥锁对象的成员函数 lock() 加锁
  explicit lock_guard(mutex_type& __m) : _M_device(__m) { _M_device.lock(); }

  // 互斥锁对象已加锁,构造函数不需要再次加锁
  lock_guard(mutex_type& __m, adopt_lock_t) noexcept : _M_device(__m) {}  // calling thread owns mutex

  // 在析构函数中,通过互斥锁对象的成员函数 unlock() 解锁
  ~lock_guard() { _M_device.unlock(); }

  // ...

 private:
  mutex_type& _M_device;  // 互斥锁对象引用
};

3.1.2 使用示例

// 编译指令:g++ -std=c++11 -pthread test.cpp
#include <iostream>
#include <mutex>
#include <thread>

#define LOOPS 10000000
int gcn = 0;         // 全局变量,通过互斥锁保护
std::mutex g_mutex;  // 全局互斥锁

void* thread_proc1() {
  for (int i = 0; i < LOOPS; i++) {
    std::lock_guard<std::mutex> guard(g_mutex);  // 构造时加锁,析构时解锁
    gcn += 1;                                    // 包含临界资源 gcn 的临界区
  }
  return NULL;
}

void* thread_proc2() {
  for (int i = 0; i < LOOPS; i++) {
    std::lock_guard<std::mutex> guard(g_mutex);
    gcn -= 1;
  }
  return NULL;
}

int main() {
  for (int i = 0; i < 10; i++) {  // 测试 10 次
    std::thread t1(thread_proc1);
    std::thread t2(thread_proc2);
    t1.join();
    t2.join();
    std::cout << "gcn " << i << ": " << gcn << '\n';  // gcn 值均为 0
    gcn = 0;
  }
}

std::lock_guard<> 相比,类模板 std::unique_lock<> 要更加灵活:

  • 可以指定在构造时是否需要加锁(支持多种加锁方式),指定在析构时是否需要解锁;
  • 可以通过 unique_lock 对象进行加锁和解锁;
  • 可以移动 unique_lock 对象关联的互斥锁。

3.1.3 一次性互斥

在某些情况下,只需要对共享数据进行一次性互斥。
例如,对于延迟初始化(lazy initialization,通常初始化开销较大)的共享数据,只需要在初始化过程中进行互斥保护,初始化完成后,相关操作不涉及数据竞争(如只读),不再需要互斥保护。

如果使用互斥锁来实现数据保护,会影响多线程的并发性能:

std::shared_ptr<some_resource> resource_ptr;
std::mutex resource_mutex;
void foo() {
  std::unique_lock<std::mutex> lk(resource_mutex);  // 每次执行,线程之间都需要互斥等待
  if (!resource_ptr) {
    resource_ptr.reset(new some_resource);
  }
  lk.unlock();
  resource_ptr->do_something();
}

C++11 提供了 std::once_flag 类和 std::call_once() 函数模板来实现一次性互斥,确保即使有多个线程、多次、同时调用某个函数(可调用对象),其只会被执行一次。

// 可调用对象 f 只被执行一次
// 通过 flag 标记执行状态,如果 f 抛出异常,则仍然可以再次尝试执行
template <class Callable, class... Args>
void call_once(std::once_flag& flag, Callable&& f, Args&&... args);
// 编译指令:g++ -std=c++11 -pthread test.cpp
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

struct some_resource {
  some_resource() {  // 需要通过互斥初始化共享资源
    std::cout << "some_resource constructor\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
  void do_something(int tid, int loop) {  // 不需要通过互斥访问共享资源
    std::cout << "do_something: " + std::to_string(tid) + ", loop " + std::to_string(loop) + "\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
};

std::shared_ptr<some_resource> resource_ptr;  // 资源指针
std::once_flag resource_flag;                 // 一个 once_flag 实例对应一次初始化操作
void init_resource(int tid, int loop) {       // 资源初始化函数
  std::cout << "init_resource: " + std::to_string(tid) + ", loop " + std::to_string(loop) + "\n";
  resource_ptr.reset(new some_resource);
}

void foo(int tid, int loop) {                               // 多线程并发执行
  std::call_once(resource_flag, init_resource, tid, loop);  // 确保 init_resource 只被执行一次
  resource_ptr->do_something(tid, loop);                    // 后续操作不再需要互斥保护
}

void thread_proc(int tid) {
  for (int i = 0; i < 3; i++) {
    foo(tid, i);
  }
}

int main() {
  std::vector<std::thread> threads;
  for (int i = 0; i < 3; i++) {
    // C++11 emplace_back 用于在容器末尾直接构造对象,避免拷贝或移动
    threads.emplace_back(std::thread(thread_proc, i));
  }
  for (auto& t : threads) {
    t.join();
  }
}

POSIX 提供的相关 API 函数为 pthread_once()

3.2 读写锁(C++14/17)

“读写锁概念”回顾

3.2.1 类定义

读写锁(共享锁)相关类在 <shared_mutex> 中定义。
与类 std::shared_mutex(C++17)相比,类 std::shared_timed_mutex(C++14)支持更多操作。

写锁成员函数:

序号成员函数(写锁/排他锁操作)shared_timed
_mutex
shared
_mutex
1void lock();
2bool try_lock();
3void unlock();
4template <class Rep, class Period>
bool try_lock_for(const std::chrono::duration<Rep, Period>& timeout_duration);
5template <class Clock, class Duration>
bool try_lock_until(const std::chrono::time_point<Clock, Duration>& timeout_time);

读锁成员函数:(相比写锁,读锁命名增加了 _shared 后缀)

序号成员函数(读锁/共享锁操作)shared_timed
_mutex
shared
_mutex
1void lock_shared();
2bool try_lock_shared();
3void unlock_shared();
4template <class Rep, class Period>
bool try_lock_shared_for(const std::chrono::duration<Rep, Period>& timeout_duration);
5template <class Clock, class Duration>
bool try_lock_shared_until(const std::chrono::time_point<Clock, Duration>& timeout_time);

读写锁的 RAII 包装器(wrapper):

  • 可以使用 std::lock_guard<std::shared_mutex>std::unique_lock<std::shared_mutex> 对“写锁”进行加锁和解锁管理;(类模板与互斥锁相同)
  • 可以使用 std::shared_lock<std::shared_mutex> 对“读锁”进行加锁和解锁管理。(类模板为读锁特有)

类模板 shared_lock<> 内部调用读写锁的 *_shared 系列成员函数,使用方式与 unique_lock<> 类似:

  • 可以指定在构造时是否需要加锁(支持多种加锁方式),在析构时是否需要解锁;
  • 可以通过 shared_lock 对象进行加锁和解锁;
  • 可以移动 shared_lock 对象关联的读写锁。

3.2.2 使用示例

// 编译指令:g++ -std=c++17 -pthread test.cpp
#include <iostream>
#include <map>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <vector>

class dns_entry { // DNS 条目
 public:
  dns_entry() = default;
  dns_entry(std::string ip, std::string time) : ip_address(ip), timestamp(time) {}
  std::string ip_address;
  std::string timestamp;
};

// 存储 DNS 条目的缓存表,用于将域名解释成对应的 IP 地址【读多】
// 会不时加入新条目,但对于给定的 DNS 条目通常在很长时间内都不会变化【写少】
class dns_cache {
  std::map<std::string, dns_entry> entries;  // 存放缓存数据
  mutable std::shared_mutex entry_mutex;     // 对 entries 进行读写保护

 public:
  dns_entry find_entry(std::string const& domain) const {
    std::shared_lock<std::shared_mutex> lk(entry_mutex);  // 保护共享的、只读的访问
    std::map<std::string, dns_entry>::const_iterator const it = entries.find(domain);
    return (it == entries.end()) ? dns_entry() : it->second;
  }

  void update_or_add_entry(std::string const& domain, dns_entry const& dns_details) {
    std::lock_guard<std::shared_mutex> lk(entry_mutex);  // 保护排他的、写访问
    entries[domain] = dns_details;
  }
};

int main() {
  dns_cache cache;

  std::vector<std::thread> threads;
  for (int i = 0; i < 10; i++) {
    if (i == 5) {  // 新增条目
      threads.emplace_back(
          [&cache] { cache.update_or_add_entry("www.baidu.com", dns_entry("182.61.200.7", "2024-3-1")); });
    }

    threads.emplace_back([&cache, i] {  // 读取条目
      auto entry = cache.find_entry("www.baidu.com");
      std::cout << std::to_string(i) + ": " + entry.ip_address + '\n';
    });
  }

  for (auto& t : threads) {
    t.join();
  }
}

3.3 信号量(C++20)

“信号量概念”回顾

3.3.1 类定义

在头文件 <semaphore> 中,包含“计数信号量”和“二值信号量”的定义:

// 计数信号量,对于同一资源,至少允许 LeastMaxValue 个并发访问。
// LeastMaxValue 是最小的最大值,而不是实际的最大值。
template <std::ptrdiff_t LeastMaxValue = /* 由实现定义 */>
class counting_semaphore;

// 二值信号量,LeastMaxValue 为 1
using binary_semaphore = std::counting_semaphore<1>;
序号成员函数说明
1constexpr explicit counting_semaphore(std::ptrdiff_t desired);构造信号量对象,其内部计数器初始化为 desired(值需 >= 0 并且 <= max()
2void release(std::ptrdiff_t update = 1);将内部计数器值增加 update,如果存在因为 acquire() 阻塞的线程,随后将根据调度策略解除线程阻塞。
3void acquire();如果内部计数器大于 0,则在将其减 1 后返回;否则线程将阻塞,直到计数器大于 0 并且能够被成功减 1
4bool try_acquire() noexcept;如果内部计数器大于 0,则尝试将其减 1,如果成功减 1,则返回 true;否则返回 false(可能存在虚假失败:计数器大于 0,却没能减 1)。
5template <class Rep, class Period>
bool try_acquire_for(const std::chrono::duration<Rep, Period>& rel_time);
如果内部计数器大于 0,并且能够成功减 1 则返回 true;否则阻塞,直到它大于 0 并且可以成功减 1,或者已超过 rel_time 指定时长。
6template <class Clock, class Duration>
bool try_acquire_until(const std::chrono::time_point<Clock, Duration>& abs_time);
如果内部计数器大于 0,并且能够成功减 1 则返回 true;否则阻塞,直到它大于 0 并且可以成功减 1,或者已到达 abs_time 指定时间点。
7static constexpr std::ptrdiff_t max() noexcept;返回内部计数器的最大可能值,该值大于或等于 LeastMaxValue

3.3.2 互斥示例

// 编译指令(需要 GCC 11 及以上):g++ -std=c++20 -pthread test.cpp
#include <iostream>
#include <semaphore>
#include <string>
#include <thread>
#include <vector>

#define PID_COUNT 5  // 消费者线程数量
#define RES_COUNT 3  // 资源数量

std::counting_semaphore sem{RES_COUNT};  // 全局计数信号量,初始计数(可访问资源数量)为 RES_COUNT

void thread_proc(int idx) {
  std::cout << "sub thread " + std::to_string(idx) + " begin\n";
  sem.acquire();  // 信号量值减 1(加锁)

  std::cout << "sub thread " + std::to_string(idx) + " processing...\n";
  sleep(idx % 2 + 1);

  sem.release();  // 信号量值加 1(解锁)
  std::cout << "sub thread " + std::to_string(idx) + " end\n";
}

int main() {
  std::vector<std::thread> threads;

  for (int i = 0; i < PID_COUNT; i++) {
    threads.emplace_back(thread_proc, i);
  }

  for (auto& thread : threads) {
    thread.join();
  }
}

3.3.3 同步示例

// 编译指令(需要 GCC 11 及以上):g++ -std=c++20 -pthread test.cpp
#include <chrono>
#include <iostream>
#include <semaphore>
#include <thread>

// 全局二值信号量,初始计数为 0(非信号状态)
std::binary_semaphore smphSignalMainToThread{0}, smphSignalThreadToMain{0};

void ThreadProc() {
  // 通过减少信号量计数,等待主线程发送开始工作的通知
  smphSignalMainToThread.acquire();
  std::cout << "[thread] Got the signal\n";

  std::this_thread::sleep_for(std::chrono::seconds(3));

  // 通过增加信号量计数,通知主线程已完成工作
  std::cout << "[thread] Send the signal\n";
  smphSignalThreadToMain.release();
}

int main() {
  std::thread thrWorker(ThreadProc);

  // 通过增加信号量计数,通知工作线程开始工作
  std::cout << "[main] Send the signal\n";
  smphSignalMainToThread.release();

  // 通过减少信号量计数,等待工作线程发送完成工作的通知
  smphSignalThreadToMain.acquire();
  std::cout << "[main] Got the signal\n";

  thrWorker.join();
}

3.4 死锁预防

死锁(deadlock)是最棘手的多线程代码问题之一,即便在大多数情形中,一切运行正常,死锁也可能不期而遇。
在编写代码时,我们可以通过一些相对简单的规则,来预防死锁。

3.4.1 死锁原因及预防建议

3.4.1.1 加锁和解锁未成对使用

死锁原因:一个线程加锁后,在没有解锁的情况下退出,导致其他线程持续阻塞。
预防建议:使用 RAII 技术将加锁和解锁代码封装起来,确保其成对使用。

3.4.1.2 线程内部重复加锁

死锁原因:在一个线程内部,重复请求加锁,可能造成死锁。
预防建议:根据需求,选择适配的锁类型,是递增锁计数(递归锁)、还是读共享(读写锁),或者有多份资源(信号量)。

3.4.1.3 线程间循环等待加锁

死锁原因:
在多个线程同时请求多个锁时,如果请求锁的顺序不一致,也容易造成死锁。
例如,线程 1 在持有锁 A 的情况下请求锁 B(顺序为 AB),而线程 2 在持有锁 B 的情况下请求锁 A(顺序为 BA),就会造成死锁。

预防建议:

void swap(type& lhs, type& rhs) {
  // 对 lhs 加锁
  // 对 rhs 加锁
  // 交换 lhs 和 rhs 的内部数据
}

// type A, B;
// 线程1 调用 swap(A, B);
// 线程2 调用 swap(B, A);

避免死锁的核心思想:只要另一个线程有可能正在(直接或间接)等待当前线程,那么当前线程千万不能反过来等待它。

避免死锁的简单规则:

  1. 避免嵌套锁:假如已经持有锁,那么不要试图请求第二个锁;
  2. 一旦持锁,就尽量避免调用由用户提供的程序接口,如果接口内部试图请求锁,就违反了“避免嵌套锁”的规则;
  3. 一旦持锁,就尽量避免 join() 等待别的线程,如果被等待线程恰好试图请求锁,就会导致死锁;
  4. 如果需要请求多个锁,尽量 采用 std::lock() 函数,一次请求全部锁
  5. 如果需要请求多个锁,并且无法一次请求全部锁,尽量在每个线程内部都依从固定顺序请求这些锁,例如 按层级(顺序)加锁

3.4.1.4 线程间循环等待 join

死锁原因:
线程之间在不涉及锁操作的情况下,由于相互等待也会发生死锁。
例如,有两个线程,各自关联一个 std::thread 实例,如果它们同时在对方的 std::thread 实例上调用 join(),就会导致死锁。

预防建议:

  • 让同一个函数 启动全部线程,join() 全部线程;
  • 参照按层级加锁,针对线程规定层级,使得每个线程仅等待层级更低的线程。

3.4.2 使用 lock() 同时加锁

C++11 标准库提供了 std::lock() 函数模板:

template <class Lockable1, class Lockable2, class... LockableN>
void lock(Lockable1& lock1, Lockable2& lock2, LockableN&... lockn);
  • 可以同时为多个互斥锁加锁,没有因为加锁顺序不同而导致的死锁风险;
  • 处理结果为 “全员共同成败”(all-or-nothing),即,或者全部成功加锁,或者全部未加锁并抛出异常。
// 编译指令:g++ -std=c++11 -pthread test.cpp
#include <chrono>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

class Employee {
  std::string id;                 // Employee 实例 ID
  std::vector<std::string> gifts; // 收到的所有礼物
  std::mutex m;                   // 为每个实例都配备互斥锁

 public:
  Employee(std::string id) : id(id) {}
  std::string output() const {
    std::string ret = "Employee " + id + " has gifts from: ";
    auto n{gifts.size()};
    for (const auto& gift : gifts) ret += gift + (--n ? ", " : "");
    return ret;
  }

  friend void exchange_gifts(Employee& e1, Employee& e2);
};

void exchange_gifts(Employee& e1, Employee& e2) {
  if (&e1 == &e2) return;  // 需要指向不同的实例

  std::cout << e1.id + " and " + e2.id + " are waiting for locks\n";

  {
    std::lock(e1.m, e2.m);                                      // 同时为两个实例加互斥锁
    std::lock_guard<std::mutex> lock_a(e1.m, std::adopt_lock);  // 通过 adopt_lock 表明已加锁
    std::lock_guard<std::mutex> lock_b(e2.m, std::adopt_lock);  // 构造函数不加锁,析构函数解锁

    std::cout << e1.id + " and " + e2.id + " got locks\n";

    e1.gifts.push_back(e2.id);
    e2.gifts.push_back(e1.id);
  }

  std::this_thread::sleep_for(std::chrono::milliseconds(696));  // 模拟耗时操作
}

int main() {
  Employee alice("Alice"), bob("Bob"), christina("Christina"), dave("Dave");

  std::vector<std::thread> threads;
  threads.emplace_back(exchange_gifts, std::ref(alice), std::ref(bob));
  threads.emplace_back(exchange_gifts, std::ref(christina), std::ref(bob));
  threads.emplace_back(exchange_gifts, std::ref(christina), std::ref(alice));
  threads.emplace_back(exchange_gifts, std::ref(dave), std::ref(bob));

  for (auto& thread : threads) thread.join();

  std::cout << alice.output() << '\n' 
            << bob.output() << '\n' 
            << christina.output() << '\n' 
            << dave.output() << '\n';
}

针对可变参数的 std::lock() 函数模板,C++17 标准提供了新的 RAII 类模板 std::scoped_lock<>,除了参数可变之外,作用与 std::lock_guard<> 完全等价。

// 头文件 include/c++/10/mutex
template <typename... _MutexTypes> // 可变参数模板(variadic template)
class scoped_lock {                // 可以接收多个互斥锁对象作为构造函数的参数列表
 public:

  // 在构造函数中,通过互斥锁对象的成员函数 lock() 加锁
  explicit scoped_lock(_MutexTypes&... __m) : _M_devices(std::tie(__m...)) { std::lock(__m...); }

  // 互斥锁对象已加锁,构造函数不需要再次加锁
  explicit scoped_lock(adopt_lock_t, _MutexTypes&... __m) noexcept : _M_devices(std::tie(__m...)) {}

  // 在析构函数中,通过互斥锁对象的成员函数 unlock() 解锁
  ~scoped_lock() {
    std::apply([](auto&... __m) { (__m.unlock(), ...); }, _M_devices);
  }

  //...

 private:
  tuple<_MutexTypes&...> _M_devices;  // 互斥锁对象引用列表
};

使用 std::scoped_lock<> 重写 exchange_gifts() 函数:

// 编译指令:g++ -std=c++17
void exchange_gifts(Employee& e1, Employee& e2) {
  if (&e1 == &e2) return; 

  {
    std::scoped_lock<std::mutex, std::mutex> guard(e1.m, e2.m); // 构造时加锁,析构时解锁
    // std::scoped_lock guard(e1.m, e2.m); // 等价版本,C++17 新特性:隐式类模板参数推导
    e1.gifts.push_back(e2.id);
    e2.gifts.push_back(e1.id);
  }

  std::this_thread::sleep_for(std::chrono::milliseconds(696));
}

3.4.3 按层级(顺序)加锁

将应用程序分层,明确每个互斥锁所属的层级。
按照程序从高层到底层的调用顺序,如果某个线程已经对高层级的互斥锁加锁,那么接下来它只能继续对相对低层级的互斥锁加锁。

// 编译指令:g++ -std=c++11 -pthread test.cpp
#include <climits>  // ULONG_MAX
#include <iostream>
#include <mutex>
#include <stdexcept>  // logic_error
#include <thread>

/**
 * 自定义互斥锁类型,保存层级编号,确保线程内的互斥锁按指定的层级顺序加锁和解锁(否则抛出异常)
 */
class hierarchical_mutex {
  std::mutex internal_mutex;               // 内部互斥锁
  unsigned long const hierarchy_value;     // 当前互斥锁层级
  unsigned long previous_hierarchy_value;  // 上一次加锁的互斥锁层级(只能比当前层级高)
  static thread_local unsigned long this_thread_hierarchy_value;  // 线程局部变量,保存每个线程最后一次加锁的层级

  void check_for_hierarchy_violation() {                   // 加锁前检测
    if (this_thread_hierarchy_value <= hierarchy_value) {  // 当前锁层级只能低于当前线程最后一次加锁的层级
      throw std::logic_error("mutex hierarchy violated");
    }
  }

  void update_hierarchy_value() {
    previous_hierarchy_value = this_thread_hierarchy_value;  // 备份上一次加锁的层级,以便在解锁时恢复
    this_thread_hierarchy_value = hierarchy_value;           // 更新当前线程最后一次加锁的层级
  }

 public:
  explicit hierarchical_mutex(unsigned long value) : hierarchy_value(value), previous_hierarchy_value(0) {}

  void lock() {
    check_for_hierarchy_violation();  // 检查锁层级是否合法
    internal_mutex.lock();            // 执行加锁操作
    update_hierarchy_value();         // 更新锁层级信息
  }

  void unlock() {                                          // 按照加锁的逆序解锁
    if (this_thread_hierarchy_value != hierarchy_value) {  // 当前解锁层级必须等于当前线程最后一次加锁的层级
      throw std::logic_error("mutex hierarchy violated");
    }
    this_thread_hierarchy_value = previous_hierarchy_value;  // 恢复上一次加锁的层级
    internal_mutex.unlock();                                 // 执行解锁操作
  }

  bool try_lock() {
    check_for_hierarchy_violation();
    if (!internal_mutex.try_lock()) return false;
    update_hierarchy_value();
    return true;
  }
};

thread_local unsigned long hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);

/**
 * hierarchical_mutex 的使用示例
 */
hierarchical_mutex high_level_mutex(100);  // 为不同层级程序上的互斥锁赋予不同的层级编号
hierarchical_mutex low_level_mutex(10);
hierarchical_mutex middle_level_mutex(60);

void low_level_func() {
  std::lock_guard<hierarchical_mutex> lk(low_level_mutex);  // low_level 加锁
  std::this_thread::sleep_for(std::chrono::seconds(1));     // 模拟 low_level 操作
}

void high_level_func() {
  std::lock_guard<hierarchical_mutex> lk(high_level_mutex);  // high_level 加锁
  std::this_thread::sleep_for(std::chrono::seconds(1));      // 模拟 high_level 操作
  low_level_func();                                          // 调用 low_level 处理
}

void thread_proc1() { high_level_func(); }

void other_stuff() {
  high_level_func();                                     // 调用 high_level 处理
  std::this_thread::sleep_for(std::chrono::seconds(1));  // 模拟 other_stuff 操作
}

void thread_proc2() {
  try {
    std::lock_guard<hierarchical_mutex> lk(middle_level_mutex);  // middle_level 加锁
    other_stuff();                                               // 间接调用 high_level 处理,锁级别不合规
  } catch (const std::exception& ex) {
    std::cout << "Exception: " << ex.what() << '\n';
  }
}

int main() {
  std::thread t1(thread_proc1);
  std::thread t2(thread_proc2);
  t1.join();
  t2.join();
}

4 原子变量

引入原子类型和原子操作的目的是支持锁无关(lock-free)的程序设计,从而降低系统开销,提高程序的执行效率。

本节仅对原子变量做简要介绍,关于内存序等更多内容可参考 cppreference 的 C Atomic operationsC++ Atomic operations 部分。

4.1 原子变量(C11)

C11 标准通过新增一个关键字 _Atomic 来支持原子类型,通过引入一个新的头文件 <stdatomic.h> 来支持原子操作。

4.1.1 原子类型

  • 可以通过关键字 _Atomic 直接声明原子类型,如 _Atomic (int)_Atomic 作为类型指定符) 或 _Atomic int_Atomic 作为类型限定符);
    • _Atomic 不能用来修饰数组、函数、原子或者限定的类型。
  • 可以使用标准库定义的原子类型别名,如 atomic_int

4.1.2 初始化原子变量

  • 如果原子变量具有静态存储期或者线程存储期,并且没有初始化,则将自动初始化为 0 值;
  • 如果原子变量具有自动存储期,并且没有初始化,则将处于不确定的状态。
/**
 * 初始化原子变量
 * 泛型函数:用 A 指代某种原子类型,用 C 指代与 A 相对应的非原子类型;
 * 用参数 value 的值初始化参数 obj 指向的原子对象,同时为该对象设置一些由实现定义的附加状态。
 * 注:在初始化期间,对当前对象的其他并发访问将会造成数据竞争,即使它们是原子操作。
 */
void atomic_init(volatile A * obj, C value);

atomic_int atom_i;
atomic_init(&atom_i, 77); // 将原子变量 atom_i 初始为 77

4.1.3 原子操作

原子操作,是对原子变量(对象)施加的操作:

  • 是不可分割的操作(indivisible operation),比如一个“读”操作、一个“写”操作,或者一个完整的“读-改-写”操作,包括:
    • 复合赋值运算符 +=-=&=|=^=;
    • 前置/后置的 ++-- 运算符;
    • <stdatomic.h> 中定义了很多执行原子操作的库函数。
  • 如果一个线程发起一个原子操作,那么在操作执行期间,其他线程不能访问相同位置的内存。
    • 可能借助于处理器的硬件指令(atomic instruction)实现;
    • 可能借助于编译器和标准库的内部锁来实现(如果处理器硬件不支持,还是要通过锁实现);
    • 在所有原子类型中,只有原子标志类型(atomic_flag)可以保证一定是锁无关的(atomic_is_lock_free() 返回真)。
// 编译指令:gcc -pthread test.c
#ifdef __STDC_NO_ATOMICS__
#error "Not support atomic facilities."
#endif

#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>

#define LOOPS 10000000  // 循环次数
_Atomic int gcn = 0;    // 全局计数变量(原子类型)

void* thread_proc1(void* arg) {
  for (int i = 0; i < LOOPS; i++) {  // 方法1:
    gcn += 1;                        // 原子复合赋值运算
    // gcn++;                        // 原子自增运算
    // gcn = gcn + 1;                // 非原子操作(无法实现互斥保护)
    // atomic_fetch_add(&gcn, 1);    // 原子加法操作
  }
  return NULL;
}

void* thread_proc2(void* arg) {
  for (int i = 0; i < LOOPS; i++) {    // 方法2:
    int new, old = atomic_load(&gcn);  // 步骤1:原子“读取值”到临时变量 old
    do {
      new = old - 1;  // 步骤2:计算(可以很复杂)新值 new

      // 如果变量 old 和 gcn 相等,则将 new 赋值给 gcn,并返回 true;
      // 否则,说明其它线程已经修改 gcn,则将 gcn 赋值给 old,并返回 false。
      // 使用原子操作函数,其它线程不需要阻塞,但是增加了线程内部的执行时间(需要重复尝试)
    } while (!atomic_compare_exchange_weak(&gcn, &old, new));  // 步骤3:原子“比较-修改/回读”操作
  }
  return NULL;
}

int main() {
  pthread_t pid1, pid2;
  for (int i = 0; i < 10; i++) {  // 测试 10 次
    pthread_create(&pid1, NULL, thread_proc1, NULL);
    pthread_create(&pid2, NULL, thread_proc2, NULL);
    pthread_join(pid1, NULL);
    pthread_join(pid2, NULL);
    printf("gcn %d: %d\n", i, gcn);  // gcn 值均为 0
    gcn = 0;
  }
}

4.2 原子变量(C++11)

4.2.1 类定义

在头文件 <atomic> 中,包含原子类型 类模板 std::atomic<> 的定义,可以将其特化成各种原子类型,如 std::atomic<int>std::atomic<int*>,以及 std::atomic<用户自定义类型>
对于 bool 和所有整型,还可以使用相应的类型别名,如 atomic_boolatomic_int

主要成员函数:(原子操作)

序号std::atomic<T> 成员函数说明
1bool is_lock_free() const noexcept;检查当前原子类型对象上的原子操作是否为无锁操作。
2T operator=( T desired ) noexcept;desired 赋值给原子变量,相当于 store(desired)。返回 desired 的拷贝。
3void store(T desired, memory_order order = memory_order_seq_cst) noexcept;desired 赋值给原子变量。
4T load(memory_order order = memory_order_seq_cst) const noexcept;返回原子变量的当前值。
5operator T() const noexcept;返回原子变量的当前值。相当于 load()
6T exchange(T desired, memory_order order = memory_order_seq_cst) noexcept;desired 赋值给原子变量,并返回赋值前的原子变量值(读-改-写操作)
7bool compare_exchange_strong(T& expected, T desired, memory_order order = memory_order_seq_cst) noexcept;比较 *this 值与 expected,如果它们按位相等,则将前者替换为 desired,返回 true(读-改-写操作);否则,将 *this 值加载到 expected,返回 false(加载操作)。
8bool compare_exchange_weak(T& expected, T desired, memory_order order = memory_order_seq_cst) noexcept;同上,可能存在虚假失败(即使 *thisexpected 相等,也可能由于线程被切出 CPU 等原因,导致 *this 更新失败,最终按照不相等的情况处理并返回) ,须配合循环使用。
9void wait(T old, memory_order order = memory_order::seq_cst) const noexcept;C++20,原子等待操作,阻塞当前线程,直到收到 notify() 通知,并且原子变量值与 old 不相等。
这种形式的“变更检测”通常比简单的轮询或纯自旋锁更高效。
10void notify_one() noexcept;C++20,原子通知操作,如果有线程在 *this 上因为原子等待操作(如 wait())阻塞,则对“至少一个”这样的线程解除阻塞。
11void notify_all() noexcept;C++20,原子通知操作,如果有线程在 *this 上因为原子等待操作(如 wait())阻塞,则对“所有”这样的线程解除阻塞。
12T fetch_add(T arg, memory_order order = memory_order_seq_cst) noexcept;将原子变量值增加 arg,返回操作执行前的原子变量值(读-改-写操作)
13T fetch_sub(T arg, memory_order order = memory_order_seq_cst) noexcept;将原子变量值减少 arg,返回值含义同上
14T fetch_and(T arg, memory_order order = memory_order_seq_cst) noexcept;将原子变量值与 arg 进行按位与,返回值含义同上
15T fetch_or(T arg, memory_order order = memory_order_seq_cst) noexcept;将原子变量值与 arg 进行按位或,返回值含义同上
16T fetch_xor(T arg, memory_order order = memory_order_seq_cst) noexcept;将原子变量值与 arg 进行按位异或,返回值含义同上
17++,--(后置自增/自减)等价操作:return fetch_add(1);return fetch_sub(1);
18++,--(前置自增/自减)等价操作:return fetch_add(1) + 1;return fetch_sub(1) - 1;
19+=,-=,&=,|=,^=(复合赋值)等价操作:return fetch_add(arg) + arg;*_sub(arg) - arg;*_and(arg) & arg;*_or(arg) | arg;*_xor(arg) ^ arg;

4.2.2 使用示例

// 编译指令:g++ -std=c++11 -pthread test.cpp
#include <atomic>
#include <iostream>
#include <thread>

#define LOOPS 10000000    // 循环次数
std::atomic<int> gcn{0};  // 全局计数变量(原子类型)

void thread_proc1() {
  for (int i = 0; i < LOOPS; i++) {  // 方法1:
    gcn += 1;                        // 原子复合赋值运算
    // gcn++;                        // 原子自增运算
    // gcn = gcn + 1;                // 非原子操作(无法实现互斥保护)
    // atomic_fetch_add(&gcn, 1);    // 原子加法操作
    // gcn.fetch_add(1);             // 原子加法操作
  }
}

void thread_proc2() {
  for (int i = 0; i < LOOPS; i++) {  // 方法2:
    int newv, old = gcn.load();      // 步骤1:原子“读取值”到临时变量 old
    do {
      newv = old - 1;  // 步骤2:计算(可以很复杂)新值 newv

      // 如果变量 old 和 gcn 相等,则将 newv 赋值给 gcn,并返回 true;
      // 否则,说明其它线程已经修改 gcn,则将 gcn 赋值给 old,并返回 false。
    } while (!gcn.compare_exchange_weak(old, newv));  // 步骤3:原子“比较-修改/回读”操作
  }
}

int main() {
  for (int i = 0; i < 10; i++) {  // 测试 10 次
    std::thread t1(thread_proc1);
    std::thread t2(thread_proc2);
    t1.join();
    t2.join();
    std::cout << "gcn " << i << ": " << gcn << '\n';  // gcn 值均为 0
    gcn = 0;
  }
}

4.2.3 基于原子类型的自旋锁

“自旋锁概念”回顾

在头文件 <atomic> 中,提供了原子标志类型 std::atomic_flag,只有“清零”(false)和“设置”(true)两种状态,是“唯一可以保证锁无关”的原子类型。

序号成员函数说明
1atomic_flag()构造原子标志对象。在 C++20 前,需要通过 ATOMIC_FLAG_INIT 初始化为清零状态;C++20 后,默认为清零状态。
2void clear(memory_order order = memory_order_seq_cst) noexcept;将原子标志置为清零状态(false
3bool test_and_set(memory_order order = memory_order_seq_cst) noexcept;将原子标志置为设置状态(true),返回操作执行前的原子标志
4bool test(memory_order order = memory_order_seq_cst) const noexcept;C++20,返回当前原子标志
5void wait(bool old, memory_order order = memory_order::seq_cst) const noexcept;C++20,原子等待操作,阻塞当前线程,直到收到 notify() 通知,并且原子标志与 old 不相等。
这种形式的“变更检测”通常比简单的轮询或纯自旋锁更高效。
6void notify_one() noexcept;C++20,原子通知操作,如果有线程在 *this 上因为原子等待操作(如 wait())阻塞,则对“至少一个”这样的线程解除阻塞。
7void notify_all() noexcept;C++20,原子通知操作,如果有线程在 *this 上因为原子等待操作(如 wait())阻塞,则对“所有”这样的线程解除阻塞。

可以使用 std::atomic_flag 提供的“测试和设置”(test_and_set())功能,实现自旋锁:

// 编译指令:g++ -std=c++11 -pthread test.cpp
#include <atomic>
#include <iostream>
#include <mutex>
#include <thread>

class spinlock {
  std::atomic_flag flag;

 public:
  spinlock() : flag(ATOMIC_FLAG_INIT) {}  // 原子标志初始化为 false,表示未被任何线程占用

  void lock() {
    // 如果 test_and_set() 返回 true,说明原子标志原先就是 true,也就是,其他线程正拥有这个标志;
    // 如果返回 false,说明原子标志原先是 false,也就是,操作前没有任何线程拥有这个标志,操作后当前线程拥有了这个标志。
    while (flag.test_and_set(std::memory_order_acquire))
      ;  // 自旋(原地打转),直到其他线程清零标志,并且当前线程成功设置标志(线程不会阻塞,没有调度开销,但会占用 CPU)
  }

  void unlock() {
    flag.clear(std::memory_order_release);  // 清零原子标志(置为 false),让其他线程获得设置(置为 true)机会
  }
};

#define LOOPS 10000000  // 循环次数
int gcn = 0;            // 全局计数变量(由自旋锁保护)
spinlock glock;         // 全局自旋锁

void thread_proc1() {
  for (int i = 0; i < LOOPS; i++) {
    std::lock_guard<spinlock> guard(glock);
    gcn = gcn + 1;
  }
}

void thread_proc2() {
  for (int i = 0; i < LOOPS; i++) {
    std::lock_guard<spinlock> guard(glock);
    gcn = gcn - 1;
  }
}

int main() {
  for (int i = 0; i < 10; i++) {  // 测试 10 次
    std::thread t1(thread_proc1);
    std::thread t2(thread_proc2);
    t1.join();
    t2.join();
    std::cout << "gcn " << i << ": " << gcn << '\n';  // gcn 值均为 0
    gcn = 0;
  }
}

5 互斥技术对比

互斥技术特点适用场景
互斥锁
(Mutex)
一次仅允许一个线程成功加锁;其他试图加锁线程将被阻塞;加锁和解锁需要在同一线程内成对使用。一般的互斥访问场景。
读写锁
(Read-Write Lock)
多个读线程可以同时加锁,写线程只能独自加锁。读操作远多于写操作的场景。
信号量
(Semaphore)
允许一定数量的线程同时加锁;加锁和解锁可以不在同一线程内成对使用。对一组资源进行互斥访问的场景;也可以处理其他线程同步问题。
自旋锁
(Spinlock)
一次仅允许一个线程成功加锁;其他试图加锁线程将会自旋等待,而不是进入阻塞状态,减少线程调度开销,增加 CPU 自旋消耗。持锁时间非常短,并且在等待锁时,不希望线程被挂起的场景。
原子变量
(Atomic Variables)
在处理器硬件支持的情况下,可以进行高效的锁无关操作。简单的数据类型和操作。

参考

  1. 朱文伟,李建英著.Linux C/C++ 服务器开发实践.清华大学出版社.2022.
  2. [英]安东尼·威廉姆斯著,吴天明译.C++并发编程实战(第2版).人民邮电出版社.2021.
  3. [美] K. N. 金(K. N. King)著,吕秀锋,黄倩译.C语言程序设计:现代方法(第2版·修订版).人民邮电出版社.2021.
  4. man7.org:linux/man-pages
  5. cppreference: concurrency support library

宁静以致远,感谢 Mark 老师。

  • 13
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值