「Linux 系统编程」线程同步概念、互斥锁和读写锁、死锁的产生

1 线程同步

在多线程编程中,当多个线程并发访问共享数据(如全局变量、静态变量等)时,如果不对访问进行控制,就极有可能导致 数据竞争数据不一致 的问题。线程同步 正是为了解决这类数据混乱问题而诞生,用于 确保多个线程在访问共享资源时的正确性和一致性



2 互斥锁

2.1 互斥锁的概念

互斥锁是实现线程同步最基本的方法。在任何时刻,只允许一个线程持有互斥锁。线程在访问共享资源前必须先成功获取锁,访问完成后释放锁。如果另一个线程试图获取一个已经被锁定的互斥锁,它将会被阻塞,直到锁被释放。

这好比只有一个钥匙的卫生间,一个人(线程)进去后会把门锁上(加锁),其他人(其他线程)必须等在门口(阻塞),直到里面的人出来(解锁)并把钥匙交给下一个人。

线程通过互斥锁实现同步示意图


2.2 Linux C 互斥锁函数

Linux 的 POSIX 线程库 pthread 提供了一系列函数来操作互斥锁,主要类型为 pthread_mutex_t

函数 功能描述
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr) 初始化一个互斥锁。attr 为属性,通常设为 NULL 表示默认属性。
int pthread_mutex_lock(pthread_mutex_t *mutex) 阻塞式 加锁。如果锁已被占用,则调用线程将阻塞,直到锁被释放。
int pthread_mutex_trylock(pthread_mutex_t *mutex) 非阻塞式 加锁。如果锁已被占用,函数会立即返回错误码 EBUSY,而不是阻塞。
int pthread_mutex_unlock(pthread_mutex_t *mutex) 解锁。释放一个已持有的互斥锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex) 销毁一个互斥锁,释放其占用的资源。

2.3 Linux C 互斥锁示例

下面是一个多线程计数器示例,包含不使用锁和使用锁两种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <pthread.h>

#define NUM_LOOPS 100000

int counter = 0; // 共享资源
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化互斥锁

// 不使用互斥锁的线程函数
void* without_mutex(void* arg) {
for (int i = 0; i < NUM_LOOPS; i++) {
counter ++ ;
}
return NULL;
}

// 使用互斥锁的线程函数
void* with_mutex(void* arg) {
for (int i = 0; i < NUM_LOOPS; i++) {
pthread_mutex_lock(&mutex); // 加锁
counter ++ ; // 临界区操作
pthread_mutex_unlock(&mutex); // 解锁
}
return NULL;
}

int main() {
pthread_t tid1, tid2;

// 不加锁的情况,最终数值在 100000 ~ 200000 之间
counter = 0;
pthread_create(&tid1, NULL, without_mutex, NULL);
pthread_create(&tid2, NULL, without_mutex, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("Without mutex: %d (expected: %d)\n", counter, 2 * NUM_LOOPS);

// 加锁的情况,最终数值为 200000
counter = 0;
pthread_create(&tid1, NULL, with_mutex, NULL);
pthread_create(&tid2, NULL, with_mutex, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("With mutex: %d (expected: %d)\n", counter, 2 * NUM_LOOPS);

pthread_mutex_destroy(&mutex); // 销毁锁

return 0;
}

不加锁时,两个线程同时对 counter 进行递增,会发生数据覆盖,最终结果会小于预期。加锁后,保证了任何时刻只有一个线程执行 counter ++ ,最终结果正确。

在 C++ 多线程中,一般采用 std::mutex / std::lock_guard / std::unique_lock 来实现线程同步。「C++ 多线程」std::mutex,lock(), unlock()「C++ 多线程」std::lock_guard 基本用法「C++ 多线程」std::unique_lock 基本用法



3 读写锁

3.1 读写锁的概念

互斥锁的粒度有时过于粗旷。考虑一种读多写少的场景(如缓存系统),多个线程同时读取数据并不会产生问题,但互斥锁只允许一个读线程进入,这无疑降低了读操作的效率。

读写锁解提供了更高的并行性。其规则如下:

  • 读模式加锁: 只要没有线程持有写锁,多个线程可以同时持有读锁。
  • 写模式加锁: 只要没有线程持有任何锁(读或写),只有一个线程可以持有写锁。

读写锁的特点可以概括为:读共享,写独占,写优先级高(默认情况下,防止写线程饥饿)。

读写锁优先级

  • 读并发与写阻塞

    当有多个读线程获得共享锁时,这些读线程可以并发地对共享资源进行读取操作。此时如果有写线程尝试获取独占锁,这个写线程会被阻塞。

    阻塞会一直持续,直到所有当前持有共享锁的读线程都释放了共享锁。

  • 写独占与读阻塞

    当一个写线程占有独占锁时,新的读线程尝试获取共享锁会被阻塞。写独占锁的优先级高于读共享锁

    这种设计是为了 防止写操作饥饿。如果读线程不断地获取共享锁,而写线程一直无法获取独占锁来执行写入操作,这会导致数据不能及时更新。

读线程同步示意图

写线程同步示意图


3.2 Linux C 读写锁函数

读写锁的类型是 pthread_rwlock_t

函数 功能描述
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr) 初始化一个读写锁。
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock) 阻塞式获取读锁。
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock) 阻塞式获取写锁。
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock) 释放持有的读锁或写锁。
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock) 销毁一个读写锁。

3.3 Linux C 读写锁示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_rwlock_t rwlock; // 读写锁
int shared_data = 0; // 共享数据

// 读线程函数
void* reader(void* arg) {
int id = *(int*)arg;
int i;
for (i = 0; i < 1000; i ++ ) {
pthread_rwlock_rdlock(&rwlock); // 获取读锁
printf("Reader %d: reads shared_data = %d\n", id, shared_data);
pthread_rwlock_unlock(&rwlock); // 释放锁
usleep(500); // 模拟其他操作
}
return NULL;
}

// 写线程函数
void* writer(void* arg) {
int id = *(int*)arg;
int i;
for (i = 0; i < 300; i ++ ) {
pthread_rwlock_wrlock(&rwlock); // 获取写锁
shared_data ++ ;
printf("Writer %d: updates shared_data to %d\n", id, shared_data);
pthread_rwlock_unlock(&rwlock); // 释放锁
usleep(2000); // 写操作耗时更长
}
return NULL;
}

int main() {
pthread_t rd_tid[3], wr_tid[2];
int reader_ids[] = {1, 2, 3};
int writer_ids[] = {1, 2};

pthread_rwlock_init(&rwlock, NULL);

// 创建2个写线程
for (int i = 0; i < 2; i ++ ) {
pthread_create(&wr_tid[i], NULL, writer, &writer_ids[i]);
}
// 创建3个读线程
for (int i = 0; i < 3; i ++ ) {
pthread_create(&rd_tid[i], NULL, reader, &reader_ids[i]);
}

// 等待线程(实际需要手动终止)
for (int i = 0; i < 2; i ++ ) pthread_join(wr_tid[i], NULL);
for (int i = 0; i < 3; i ++ ) pthread_join(rd_tid[i], NULL);

pthread_rwlock_destroy(&rwlock);

return 0;
}

多个读线程可以同时读取数据,打印信息快速且连续。而当写线程工作时,它会独占锁,此时所有读线程和其他写线程都会被阻塞,直到写操作完成。显著提高了读多写少场景下的程序性能。

在 C++ 多线程中,通常会使用 std::shared_lock 配合 std::unique_lock 来实现读写锁(读共享、写独占)。「C++ 多线程」std::shared_mutex,std::shared_lock



4 死锁

4.1 死锁的概念

死锁是指两个或多个线程在执行过程中,因 争夺资源 而造成的一种相互等待(循环等待)的现象。若无外力干涉,这些线程都将无法继续推进。

死锁产生的条件

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

4.2 死锁产生的场景

  1. 重复加锁

一个线程已经持有一个锁,又再次尝试获取它。默认属性的互斥锁不支持重入,会导致该线程永久阻塞自己。

解决方法: 使用可重入锁(递归锁),通过设置互斥锁属性 PTHREAD_MUTEX_RECURSIVE 实现。


  1. 加锁顺序不一致

这是最常见的死锁原因。线程 T1 先锁 A 再锁 B,而线程 T2 先锁 B 再锁 A。在并发执行时,可能发生 T1 锁住 A 的同时 T2 锁住了 B,双方都在等待对方释放另一个锁,导致死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 线程1
void* thread1_func(void* arg) {
pthread_mutex_lock(&mutexA);
sleep(1); // 增加死锁概率
pthread_mutex_lock(&mutexB); // 此处阻塞,等待线程2释放mutexB
// ... 操作
pthread_mutex_unlock(&mutexB);
pthread_mutex_unlock(&mutexA);
return NULL;
}

// 线程2
void* thread2_func(void* arg) {
pthread_mutex_lock(&mutexB);
sleep(1);
pthread_mutex_lock(&mutexA); // 此处阻塞,等待线程1释放mutexA
// ... 操作
pthread_mutex_unlock(&mutexA);
pthread_mutex_unlock(&mutexB);
return NULL;
}

两个线程互相等待产生死锁

解决方法: 定义统一的加锁顺序。所有需要同时获取锁 A 和 B 的线程,都必须按照先 A 后 B 的顺序申请。

在 C++ 多线程中,也可以通过 std::lock(mtx1, mtx2) 一次性分配所有互斥锁资源来避免死锁。「C++ 多线程」死锁及 C++ 多线程解决死锁方法



「Linux 系统编程」线程同步概念、互斥锁和读写锁、死锁的产生
https://marisamagic.github.io/2025/09/19/20250919/
作者
MarisaMagic
发布于
2025年9月19日
许可协议