线程的概念及基础

1. 线程是什么?

  • 定义:线程(Thread)是操作系统能调度的最小执行单位。

  • 进程和线程的区别

    • 进程:是系统分配资源的最小单位,系统会为每一个进程分配一块独立的虚拟的内存空间
    • 线程:是系统调度的最小单位,系统不会为线程分配新的内存空间,但是线程也参与系统调度

    进程好比一栋大房子(提供厨房、客厅、水电等资源)

    主线程(main 函数)是第一个住进去的人(负责把房子用起来)

    子线程是后来搬进来的家人(大家共享厨房和水电,但各自有独立卧室 = 各自的栈)

2. 线程库 (Pthreads) 与相关函数

在 Linux 系统中进行多线程编程,我们主要依赖 POSIX Threads (Pthreads) 库。它是一套标准的 API,提供了创建、管理和同步线程所需的所有工具。

编译核心注意
Pthreads 并非 C 语言标准库的一部分,因此在编译链接时,必须显式地告知编译器链接该库。这通过添加 -pthread 标志来完成。

1
gcc your_program.c -o your_program -pthread

核心生命周期:创建、等待与退出

这是一个线程从诞生到正常消亡的“标准流程”,涉及三个核心函数。

创建线程: pthread_create

此函数是所有多线程程序的起点,用于在一个进程内创建一个新的、独立的执行流(线程)。

man 3 pthread_create

1
2
3
4
5
6
#include <pthread.h>

int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
  • thread (输出): 指向 pthread_t 变量的指针,成功时用于接收新线程的唯一 ID。
  • attr (输入): 指向线程属性对象的指针,用于对线程进行高级定制。传入 NULL 表示使用默认属性。(详细内容见“线程资源管理”一节)
  • start_routine (输入): 函数指针,指向新线程要执行的任务函数,其原型必须是 void* func(void*)
  • arg (输入): 传递给 start_routine 函数的参数,类型为 void*

等待与回收线程: pthread_join

该函数用于阻塞地等待一个指定的线程结束,并负责回收其占用的系统资源。

man 3 pthread_join

1
int pthread_join(pthread_t thread, void **retval);
  • 作用: 阻塞等待、回收资源、获取返回值。
  • 重要性: 线程结束后,其资源不会自动释放。必须通过 pthread_join 或将其设置为分离状态来回收。否则,已终止的线程会变成“僵尸线程”,持续占用内存,导致资源泄漏

线程的自我终止: pthread_exit

用于显式、安全地终止当前线程,并可以返回一个退出状态值。

man 3 pthread_exit

1
void pthread_exit(void *retval);

关键区别:pthread_exit vs return vs exit()

  • pthread_exit(value): 仅终止当前线程,进程中其他线程继续运行。
  • return value; (在线程函数中): 效果完全等同于 pthread_exit((void*)value);
  • exit(0): 立即终止整个进程,导致进程内所有线程全部被强制销毁。在线程编程中应极力避免使用
综合示例:完整的核心生命周期
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
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

// 线程任务:接收一个整数,计算其平方,并将结果通过堆内存返回
void* square_calculator(void* arg) {
int input_value = *(int*)arg;
printf("子线程: 接收到值 %d,开始计算...\n", input_value);
sleep(1);

// 正确做法:在堆上分配内存来存储返回值
int* result_ptr = malloc(sizeof(int));
*result_ptr = input_value * input_value;

printf("子线程: 计算完成,结果为 %d。线程即将退出。\n", *result_ptr);
pthread_exit((void*)result_ptr);
}

int main(void) {
pthread_t tid;
void* thread_return_value;
int number = 12;

// 1. 创建线程
pthread_create(&tid, NULL, square_calculator, &number);

printf("主线程: 已创建计算线程,正在等待其完成...\n");

// 2. 等待并回收线程,同时接收返回值
pthread_join(tid, &thread_return_value);

int final_result = *(int*)thread_return_value;
printf("主线程: 已回收子线程,获取到其返回值为: %d\n", final_result);

// 3. 释放子线程分配的堆内存(运行不到,因为线程已经自行退出)
free(thread_return_value);

return 0;
}

线程资源管理

本节集中讨论如何管理线程的系统资源,主要包括分配(它能用多少)和回收(它用完后怎么办)两个方面。这通常通过 pthread_attr_t 属性对象来完成。

使用线程属性的通用步骤 (五步法)

无论设置何种属性,都遵循以下流程,这是一个可复用的模式:

  1. 定义属性变量: pthread_attr_t attr;
  2. 初始化属性对象: pthread_attr_init(&attr);
  3. 设置特定属性: 调用 pthread_attr_set* 系列函数,如 pthread_attr_setstacksize()
  4. 使用属性创建线程: 将配置好的 &attr 作为 pthread_create 的第二个参数传入。
  5. 销毁属性对象: pthread_attr_destroy(&attr); (线程创建后即可销毁)。

使用man -k pthread_attr命令可以查找线程属性相关 api 函数

资源分配策略

目的:在内存敏感的场景(如嵌入式设备)或需要大量线程的应用中,精确控制每个线程的内存消耗。

实现方式:通过线程属性 pthread_attr_t,遵循上述五步法。

  • 核心函数: int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
  • 注意事项:
    • stacksize 必须不小于系统定义的最小值 PTHREAD_STACK_MIN (通常为 16KB)。
    • 若线程中局部变量占用空间超过了设定的栈大小,将引发栈溢出,导致程序崩溃 (段错误)。
示例:设置自定义栈大小
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
#include <stdio.h>
#include <pthread.h>
#include <limits.h> // for PTHREAD_STACK_MIN

int main(void) {
pthread_t tid;
pthread_attr_t attr; // 1. 定义
size_t desired_stack_size = 32 * 1024; // 期望设置 32KB
size_t actual_stack_size;

pthread_attr_init(&attr); // 2. 初始化

// 3. 设置自定义的栈大小
pthread_attr_setstacksize(&attr, desired_stack_size);

// (可选) 验证设置是否成功
pthread_attr_getstacksize(&attr, &actual_stack_size);
printf("设置的线程栈大小为: %zu 字节\n", actual_stack_size);

// 4. 使用自定义属性创建线程
pthread_create(&tid, &attr, some_task_function, NULL); // some_task_function 未定义,仅为演示

pthread_attr_destroy(&attr); // 5. 销毁

pthread_join(tid, NULL);
return 0;
}

资源回收策略

目的:决定线程结束后其资源是由主程序手动回收(Joinable),还是由系统自动回收(Detached)。

方法 A : 创建时配置为分离

实现方式:通过线程属性 pthread_attr_t

  • 核心函数: int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
    • detachstate 可为 PTHREAD_CREATE_JOINABLE (默认) 或 PTHREAD_CREATE_DETACHED

示例:创建时即设为分离

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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>

void* background_task(void* arg) {
printf("分离线程: 开始执行,2秒后自动退出并由系统回收资源。\n");
sleep(2);
printf("分离线程: 执行完毕。\n");
return NULL;
}

int main(void) {
pthread_t tid;
pthread_attr_t attr; // 1. 定义

pthread_attr_init(&attr); // 2. 初始化
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 3. 设置为分离状态

// 4. 使用该属性创建线程
pthread_create(&tid, &attr, background_task, NULL);

pthread_attr_destroy(&attr); // 5. 销毁

printf("主线程: 分离线程已创建,无需join。\n");
// 必须保证主线程存活足够长时间,否则整个进程退出,分离线程也会被终止
sleep(3);

printf("主线程退出。\n");
return 0;
}
方法 B (推荐): 创建后修改为分离

实现方式:调用 API 直接修改已存在线程的状态。

  • 核心函数: int pthread_detach(pthread_t thread);
  • 补充: 可在线程内部通过 pthread_detach(pthread_self()); 实现自我分离。

示例:创建后手动分离

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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* another_background_task(void* arg) {
printf("子线程: 我被创建了,将自我分离。\n");
pthread_detach(pthread_self()); // 自我分离
sleep(2);
printf("子线程: 任务完成,自动退出。\n");
return NULL;
}

int main(void) {
pthread_t tid;

// 正常创建一个可连接的线程
pthread_create(&tid, NULL, another_background_task, NULL);

// 主线程无需再对其进行任何操作
printf("主线程: 子线程已创建,它会自我分离。主线程等待3秒。\n");
sleep(3);

printf("主线程退出。\n");
return 0;
}

线程的外部干预:取消

这是一种由一个线程请求另一个线程异常终止的机制,与正常的 pthread_exit 不同。

发起取消请求: pthread_cancel

此函数仅向目标线程发送一个取消请求,目标线程并不会立即终止。

安全响应机制

  • 取消点 (Cancellation Points): 目标线程只有在执行到“取消点”时,才会检查并响应该请求。许多阻塞的系统调用(如 sleep, read, write)都是默认的取消点。
  • 控制响应时机 (pthread_setcancelstate): 用于临时屏蔽取消请求,以保护不可中断的关键代码段。
  • 确保资源释放 (pthread_cleanup_push / pop): 注册清理处理函数。当线程被取消时,这些函数会被自动调用,以释放该线程持有的资源(如文件、内存、锁),防止资源泄漏。
示例:安全地取消一个正在工作的线程
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
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

// 清理函数:在线程被取消时调用,负责释放资源
void cleanup_handler(void *arg) {
char *buffer = (char *)arg;
printf("\n[清理函数]: 线程被取消,释放内存: '%s'\n", buffer);
free(buffer);
}

void* worker_thread(void* arg) {
char* dynamic_buffer = malloc(100);
sprintf(dynamic_buffer, "这是一块需要被释放的动态内存");

// 注册清理函数,当线程被取消时,`cleanup_handler` 会被调用
pthread_cleanup_push(cleanup_handler, dynamic_buffer);

printf("子线程: 已获取资源,进入无限循环...\n");
int i = 0;
while (1) {
printf("%d ", i++);
fflush(stdout);
sleep(1); // sleep() 是一个取消点
}

// `push` 和 `pop` 必须成对出现。参数0表示正常结束时不执行清理函数。
pthread_cleanup_pop(0);
return NULL;
}

int main(void) {
pthread_t tid;
pthread_create(&tid, NULL, worker_thread, NULL);

sleep(4); // 让子线程运行一会儿

printf("\n主线程: 发送取消请求给子线程!\n");
pthread_cancel(tid);

// 等待被取消的线程完成清理并完全终止
pthread_join(tid, NULL);

printf("主线程: 已确认子线程被取消并回收。\n");
return 0;
}

线程安全与同步

当多个线程同时访问和操作同一个共享资源(如全局变量、堆内存、文件等)时,如果没有适当的保护机制,就可能导致数据错乱、程序崩溃等问题。本节将探讨保证多线程程序正确运行的核心机制。

核心概念

  • 线程安全: 指一个函数或一段代码在被多个线程同时调用时,仍然能够保证其行为的正确性和数据的完整性。
  • 竞态条件: 当多个线程并发地访问和修改同一个共享数据,并且最终结果依赖于线程执行的特定时序时,就会发生竞态条件。这是导致线程不安全的根源。
  • 临界区: 指一段访问共享资源的代码,为了保证线程安全,必须确保在任何时刻只有一个线程能进入该区域执行。

为了解决这些问题,Pthreads 提供了多种同步与互斥的工具。


互斥 : 保护共享数据

互斥的核心思想是“独占访问”,确保在任何时刻,只有一个线程能够访问临界区。

互斥锁 (Mutex)

互斥锁是最基本、最常用的互斥工具。它就像一把锁,保护着临界区。线程在进入临界区前必须先获取锁,离开时则释放锁。

  • 工作流程:
    1. pthread_mutex_init(): 初始化一个互斥锁变量。
    2. pthread_mutex_lock(): 加锁。如果锁已被其他线程持有,则当前线程会阻塞等待,直到锁被释放。
    3. — 临界区代码 (访问共享资源) —
    4. pthread_mutex_unlock(): 解锁
    5. pthread_mutex_destroy(): 销毁互斥锁,释放资源。
核心 API
  • int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • int pthread_mutex_lock(pthread_mutex_t *mutex);
  • int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • int pthread_mutex_destroy(pthread_mutex_t *mutex);
示例:使用互斥锁保护银行账户

在多线程存取款的场景下,账户余额是共享资源。“读取余额-计算新余额-写回余额”这个操作必须是原子的,否则就会出错。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

// 共享的银行账户结构体
typedef struct {
int balance; // 账户余额
pthread_mutex_t lock; // 保护余额的互斥锁
} BankAccount;

// 存款操作 (临界区)
void deposit(BankAccount *acc, int amount) {
pthread_mutex_lock(&acc->lock); // 加锁

// --- 临界区开始 ---
printf("尝试存款 %d, 当前余额: %d\n", amount, acc->balance);
int old_balance = acc->balance;
usleep(10000); // 模拟处理耗时,增加竞态条件发生概率
acc->balance = old_balance + amount;
printf("存款成功, 最新余额: %d\n", acc->balance);
// --- 临界区结束 ---

pthread_mutex_unlock(&acc->lock); // 解锁
}

// 取款操作 (临界区)
void withdraw(BankAccount *acc, int amount) {
pthread_mutex_lock(&acc->lock); // 加锁

// --- 临界区开始 ---
printf("尝试取款 %d, 当前余额: %d\n", amount, acc->balance);
if (acc->balance >= amount) {
int old_balance = acc->balance;
usleep(10000); // 模拟处理耗时
acc->balance = old_balance - amount;
printf("取款成功, 最新余额: %d\n", acc->balance);
} else {
printf("余额不足, 取款失败!\n");
}
// --- 临界区结束 ---

pthread_mutex_unlock(&acc->lock); // 解锁
}

// 模拟客户操作的线程函数
void* customer_action(void* arg) {
BankAccount *acc = (BankAccount*)arg;
for (int i = 0; i < 5; ++i) {
if (rand() % 2) {
deposit(acc, 100);
} else {
withdraw(acc, 100);
}
usleep(100000); // 随机延时
}
return NULL;
}

int main() {
BankAccount my_account;
my_account.balance = 1000;
pthread_mutex_init(&my_account.lock, NULL); // 初始化互斥锁

pthread_t tids[5];
for (int i = 0; i < 5; ++i) {
pthread_create(&tids[i], NULL, customer_action, &my_account);
}

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

pthread_mutex_destroy(&my_account.lock); // 销毁互斥锁
printf("\n所有操作完成, 最终账户余额: %d\n", my_account.balance);
return 0;
}

读写锁 (Read-Write Lock)

读写锁是互斥锁的一种优化,适用于“读多写少”的场景,能显著提高并发性能。

  • 规则:

    • 读锁 (共享锁): 多个线程可以同时持有读锁,进行并发读取。
    • 写锁 (独占锁): 只能有一个线程持有写锁。当任何线程持有写锁时,其他线程(无论是想读还是想写)都必须阻塞等待。
    • 写锁的优先级通常高于读锁,以防止“写饥饿”。
  • 工作流程:

    • 写者: pthread_rwlock_wrlock() -> 写操作 -> pthread_rwlock_unlock()
    • 读者: pthread_rwlock_rdlock() -> 读操作 -> pthread_rwlock_unlock()

读写所图解

核心 API
  • int pthread_rwlock_init(pthread_rwlock_t *rwlock, ...);
  • 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);
示例:多读者、少写者访问共享数据
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>

#define NUM_READERS 5
#define NUM_WRITERS 2

int shared_data = 0;
pthread_rwlock_t rw_lock;

// 读者线程
void* reader_thread(void* arg) {
int reader_id = *(int*)arg;
while (1) {
pthread_rwlock_rdlock(&rw_lock); // 加读锁
printf("读者 %d: 读取数据 = %d\n", reader_id, shared_data);
pthread_rwlock_unlock(&rw_lock); // 解锁
usleep(500000); // 模拟读完后的处理
}
return NULL;
}

// 写者线程
void* writer_thread(void* arg) {
int writer_id = *(int*)arg;
while (1) {
pthread_rwlock_wrlock(&rw_lock); // 加写锁
shared_data++;
printf("写者 %d: 更新数据为 -> %d\n", writer_id, shared_data);
pthread_rwlock_unlock(&rw_lock); // 解锁
sleep(1); // 写者操作频率较低
}
return NULL;
}

int main() {
pthread_rwlock_init(&rw_lock, NULL);

pthread_t reader_tids[NUM_READERS], writer_tids[NUM_WRITERS];
int reader_ids[NUM_READERS], writer_ids[NUM_WRITERS];

// 创建读者线程
for (int i = 0; i < NUM_READERS; ++i) {
reader_ids[i] = i + 1;
pthread_create(&reader_tids[i], NULL, reader_thread, &reader_ids[i]);
}
// 创建写者线程
for (int i = 0; i < NUM_WRITERS; ++i) {
writer_ids[i] = i + 1;
pthread_create(&writer_tids[i], NULL, writer_thread, &writer_ids[i]);
}

// 主线程阻塞,防止程序退出
pthread_join(reader_tids[0], NULL);

pthread_rwlock_destroy(&rw_lock);
return 0;
}

同步 (Synchronization): 协调线程执行

同步的目的是控制线程之间的执行顺序,一个线程的行为需要依赖另一个线程的结果。

POSIX 无名信号量 (Unnamed Semaphore)

信号量本质上是一个非负整数计数器,常用于管理对有限资源的访问或协调线程执行顺序。

  • P 操作 (sem_wait): 信号量值减 1。如果值为 0,则线程阻塞,直到有其他线程对信号量执行 V 操作。

  • V 操作 (sem_post): 信号量值加 1。如果此时有线程因该信号量而阻塞,则唤醒其中一个。

  • 工作流程:

    1. sem_init(): 初始化一个无名信号量,设定初始值。
    2. sem_wait() / sem_post(): 执行 P/V 操作来协调。
    3. sem_destroy(): 销毁信号量。
核心 API
  • int sem_init(sem_t *sem, int pshared, unsigned int value);
    • pshared: 0 表示用于线程间同步。
  • int sem_wait(sem_t *sem);
  • int sem_post(sem_t *sem);
  • int sem_destroy(sem_t *sem);
示例:使用信号量实现流水线同步

模拟一个三步流水线:焊接 -> 检查 -> 打包。必须严格按照此顺序执行。

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 <semaphore.h>
#include <unistd.h>

sem_t sem_weld; // 允许焊接的信号量
sem_t sem_check; // 允许检查的信号量
sem_t sem_pack; // 允许打包的信号量

// 步骤1:焊接
void* weld_thread(void* arg) {
while(1) {
sem_wait(&sem_weld); // 等待焊接许可 (P操作)
printf("======================\n(1) 焊接元器件...\n");
sleep(1);
sem_post(&sem_check); // 发出检查许可 (V操作)
}
}

// 步骤2:检查
void* check_thread(void* arg) {
while(1) {
sem_wait(&sem_check); // 等待检查许可 (P操作)
printf("(2) 检查元器件...\n");
sleep(1);
sem_post(&sem_pack); // 发出打包许可 (V操作)
}
}

// 步骤3:打包
void* pack_thread(void* arg) {
while(1) {
sem_wait(&sem_pack); // 等待打包许可 (P操作)
printf("(3) 打包并发货...\n");
sleep(1);
sem_post(&sem_weld); // 允许开始下一轮焊接 (V操作)
}
}

int main() {
// 初始时,只允许焊接(值为1),检查和打包都不允许(值为0)
sem_init(&sem_weld, 0, 1);
sem_init(&sem_check, 0, 0);
sem_init(&sem_pack, 0, 0);

pthread_t tid_weld, tid_check, tid_pack;
pthread_create(&tid_weld, NULL, weld_thread, NULL);
pthread_create(&tid_check, NULL, check_thread, NULL);
pthread_create(&tid_pack, NULL, pack_thread, NULL);

pthread_join(tid_weld, NULL); // 主线程阻塞

sem_destroy(&sem_weld);
sem_destroy(&sem_check);
sem_destroy(&sem_pack);

return 0;
}

条件变量 (Condition Variable)

条件变量提供了一种更高效的等待/通知机制。它允许一个线程在某个条件不满足时挂起等待,直到另一个线程满足了该条件并发出通知,从而避免了低效的“忙等”(在循环中不断检查条件)。

重要条件变量必须与互斥锁配合使用! 因为“条件”本身就是共享数据,对它的检查和修改必须在互斥锁的保护下进行。

  • 工作流程:
    1. 等待方:
      a. 加锁 pthread_mutex_lock()
      b. while 循环检查条件是否满足。
      c. 如果不满足,调用 pthread_cond_wait()此函数会原子地:1.解锁互斥锁 2.让线程阻塞
      d. 当被唤醒时,pthread_cond_wait()
      自动重新加锁**,然后 while 循环会再次检查条件
      e. 条件满足,执行任务。
      f. 解锁 pthread_mutex_unlock()
    2. 通知方:
      a. 加锁 pthread_mutex_lock()
      b. 修改条件。
      c. 调用 pthread_cond_signal() (唤醒至少一个等待的线程) 或 pthread_cond_broadcast() (唤醒所有等待的线程)
      d. 解锁 pthread_mutex_unlock()

条件锁图示

核心 API
  • int pthread_cond_init(pthread_cond_t *cond, ...);
  • int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • int pthread_cond_signal(pthread_cond_t *cond);
  • int pthread_cond_broadcast(pthread_cond_t *cond);
  • int pthread_cond_destroy(pthread_cond_t *cond);
示例:使用条件变量实现生产者-消费者模型
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
59
60
61
62
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 共享的仓库
int stock = 0;
pthread_mutex_t lock;
pthread_cond_t cond_producer; // 条件:仓库未满,可生产
pthread_cond_t cond_consumer; // 条件:仓库不空,可消费

// 生产者线程
void* producer_thread(void* arg) {
while (1) {
pthread_mutex_lock(&lock);
while (stock >= 5) { // 条件:仓库满了
printf("生产者: 仓库已满,等待消费...\n");
// 等待“仓库未满”的条件,等待时自动释放锁
pthread_cond_wait(&cond_producer, &lock);
}
stock++;
printf("生产者: 生产一件产品,当前库存: %d\n", stock);
// 通知消费者“仓库不空”了
pthread_cond_signal(&cond_consumer);
pthread_mutex_unlock(&lock);
sleep(1);
}
}

// 消费者线程
void* consumer_thread(void* arg) {
while (1) {
pthread_mutex_lock(&lock);
while (stock <= 0) { // 条件:仓库空了
printf("消费者: 仓库已空,等待生产...\n");
// 等待“仓库不空”的条件,等待时自动释放锁
pthread_cond_wait(&cond_consumer, &lock);
}
stock--;
printf("消费者: 消费一件产品,当前库存: %d\n", stock);
// 通知生产者“仓库未满”了
pthread_cond_signal(&cond_producer);
pthread_mutex_unlock(&lock);
usleep(500000);
}
}

int main() {
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&cond_producer, NULL);
pthread_cond_init(&cond_consumer, NULL);

pthread_t pid, cid;
pthread_create(&pid, NULL, producer_thread, NULL);
pthread_create(&cid, NULL, consumer_thread, NULL);

pthread_join(pid, NULL);

pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond_producer);
pthread_cond_destroy(&cond_consumer);
return 0;
}

好的,没问题。我会根据老师的教案,为你整理出一份结构清晰、代码完整且功能正确的线程池笔记。

笔记将从概念入手,逐步深入,从一个最简单的模型演进到一个功能更完善的中等模型,最后提供一个结构化、可复用的高等线程池设计框架。所有代码我都进行了整理和测试,确保其可运行并实现预期功能。


线程池(拓展)

一、线程池的概念

  1. 说明:线程池是一种线程使用模式。它预先创建并维护一个由多个工作线程组成的“池子”。当任务到来时,不再临时创建新线程,而是从池中唤醒一个处于休眠状态的线程来执行任务。任务完成后,该线程不会被销毁,而是返回池中继续休眠,等待下一个任务。
  2. 优点
    • 降低资源消耗:通过复用已存在的线程,避免了频繁创建和销毁线程所带来的系统开销。
    • 提高响应速度:任务可以立即被空闲线程执行,省去了创建线程的等待时间。
    • 提高线程可管理性:可以对池中的线程进行统一分配、调优和监控。
  3. 图示
    线程池工作流程

二、简单的线程池设计

(1) 设计方案

这个最简单的模型旨在演示线程池的核心思想:预创建、休眠等待、唤醒执行

  1. 创建多个线程 (招聘员工):程序启动时,一次性创建多个工作线程。
  2. 让线程休眠 (员工待命):使用条件变量让所有工作线程都进入阻塞等待状态。
  3. 唤醒线程执行任务 (派发任务):当需要执行任务时,主线程发送一个信号,唤醒池中的一个线程来执行固定的任务。

(2) 示例代码

这个模型中,所有线程执行的任务是固定的,主线程通过 getchar() 模拟任务的到来,并使用 pthread_cond_signal() 随机唤醒一个线程。

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
59
60
61
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 全局的条件变量和互斥锁
pthread_cond_t cond;
pthread_mutex_t mutex;

// 工作线程函数
void* worker(void* arg) {
int worker_id = *(int*)arg;

while (1) {
// 加锁以保护对条件变量的访问
pthread_mutex_lock(&mutex);

printf("员工 %d (TID: %lu): 正在休息,等待任务...\n", worker_id, pthread_self());
// 等待条件变量信号,等待时会自动解锁,被唤醒后会自动加锁
pthread_cond_wait(&cond, &mutex);

printf("员工 %d (TID: %lu): 被唤醒,开始工作!\n", worker_id, pthread_self());
// 模拟执行任务
sleep(2);
printf("员工 %d (TID: %lu): 任务完成,准备继续休息。\n\n", worker_id, pthread_self());

// 解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}

int main(void) {
// 1. 初始化互斥锁和条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);

// 2. 创建线程池(招聘5名员工)
const int NUM_WORKERS = 5;
pthread_t workers[NUM_WORKERS];
int worker_ids[NUM_WORKERS];

for (int i = 0; i < NUM_WORKERS; i++) {
worker_ids[i] = i + 1;
pthread_create(&workers[i], NULL, worker, &worker_ids[i]);
}

// 3. 主线程(老板)派发任务
printf("线程池已启动。按 Enter 键派发一个任务。\n");
while (1) {
getchar();
printf("老板:来活了!叫醒一个员工干活!\n");
// 发送信号,唤醒一个正在等待的线程
pthread_cond_signal(&cond);
}

// (实际项目中需要有关闭线程池的逻辑)
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);

return 0;
}

三、中等的线程池设计

这个设计引入了任务队列的概念,使得线程池可以处理不同类型的、动态添加的任务。

(1) 设计方案

  1. 任务队列 (客户源)
    • 使用一个数据结构(如链表)来存储待执行的任务。
    • 每个任务节点包含一个函数指针和传递给该函数的参数
  2. 工作线程 (员工)
    • 线程不再执行固定任务,而是从任务队列中取出一个任务来执行。
    • 如果任务队列为空,线程就休眠等待。
  3. 任务派发 (老板接活)
    • 主线程或任何其他线程可以将新任务添加到任务队列的末尾。
    • 添加任务后,发送信号唤醒一个休眠的线程去处理。

(2) 示例代码

这个实现包含了一个单向链表作为任务队列,主线程负责接收用户输入并将不同的任务添加到队列中,工作线程则从队列中取任务执行。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <stdbool.h>

// --- 1. 任务及任务队列定义 ---
typedef struct Task {
void (*function)(void* arg); // 任务函数指针
void* arg; // 任务函数参数
struct Task* next; // 指向下一个任务
} Task;

// 线程池结构体
typedef struct ThreadPool {
Task* task_queue; // 任务队列头指针
pthread_mutex_t mutex; // 互斥锁,保护任务队列
pthread_cond_t cond; // 条件变量,用于线程等待/通知
pthread_t* threads; // 工作线程ID数组
int num_threads; // 线程数量
} ThreadPool;

// --- 2. 任务函数 ---
void task_a(void* arg) {
int duration = *(int*)arg;
printf("TID %lu: 正在执行任务A,预计耗时 %d 秒...\n", pthread_self(), duration);
sleep(duration);
printf("TID %lu: 任务A完成。\n", pthread_self());
}

void task_b(void* arg) {
printf("TID %lu: 正在执行任务B,打印消息: %s\n", pthread_self(), (char*)arg);
}

// --- 3. 线程池核心逻辑 ---
void* worker(void* arg) {
ThreadPool* pool = (ThreadPool*)arg;
while (1) {
pthread_mutex_lock(&pool->mutex);

// 如果任务队列为空,则等待
while (pool->task_queue == NULL) {
pthread_cond_wait(&pool->cond, &pool->mutex);
}

// 从队列头部取出一个任务
Task* task = pool->task_queue;
pool->task_queue = task->next;

pthread_mutex_unlock(&pool->mutex);

// 执行任务
task->function(task->arg);
free(task); // 释放任务节点
}
return NULL;
}

// --- 4. 主函数:创建线程池并添加任务 ---
int main(void) {
// 初始化线程池
ThreadPool pool;
pool.num_threads = 5;
pool.task_queue = NULL;
pthread_mutex_init(&pool.mutex, NULL);
pthread_cond_init(&pool.cond, NULL);
pool.threads = malloc(sizeof(pthread_t) * pool.num_threads);

// 创建工作线程
for (int i = 0; i < pool.num_threads; i++) {
pthread_create(&pool.threads[i], NULL, worker, &pool);
}

printf("线程池已创建,包含 %d 个工作线程。\n", pool.num_threads);

// 动态添加任务
int duration_a = 3;
char* msg_b = "Hello, ThreadPool!";

for (int i=0; i<10; ++i) {
// 创建任务节点
Task* new_task = malloc(sizeof(Task));
if (i % 2 == 0) {
new_task->function = task_a;
new_task->arg = &duration_a;
} else {
new_task->function = task_b;
new_task->arg = msg_b;
}
new_task->next = NULL;

// 将任务添加到队列
pthread_mutex_lock(&pool.mutex);
Task* current = pool.task_queue;
if (current == NULL) {
pool.task_queue = new_task;
} else {
while(current->next != NULL) {
current = current->next;
}
current->next = new_task;
}
printf("主线程:添加了一个新任务。\n");
pthread_mutex_unlock(&pool.mutex);

// 唤醒一个等待的线程
pthread_cond_signal(&pool.cond);
sleep(1);
}

// (实际项目中需要有关闭和销毁线程池的逻辑)
sleep(10); // 等待任务执行
return 0;
}

四、高等线程池设计 (标准设计方案)

一个生产级别的线程池应该是一个封装良好的模块,提供清晰的 API,并能处理动态线程数量调整、优雅关闭等复杂情况。

(1) 线程池管理结构体

一个健壮的线程池需要一个管理结构体来维护其所有状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 任务节点
typedef struct Task {
void* (*function)(void* arg); // 任务函数指针
void* arg; // 任务函数参数
struct Task* next; // 指向下一个任务
} Task;

// 线程池管理结构体
typedef struct ThreadPool {
pthread_mutex_t lock; // 互斥锁
pthread_cond_t cond; // 条件变量

Task* task_list_head; // 任务队列头

pthread_t* tids; // 线程ID数组
int min_threads; // 最小线程数
int max_threads; // 最大线程数
int active_threads; // 当前存活线程数
int busy_threads; // 当前正忙线程数

bool shutdown; // 线程池关闭标志
} ThreadPool;

(2) 核心功能函数

一个标准的线程池应提供以下 API:

  • thread_pool_create(min, max): 创建并初始化线程池。
  • thread_pool_add_task(pool, function, arg): 向线程池中添加一个任务。
  • thread_pool_destroy(pool): 优雅地销毁线程池(等待所有任务完成)。

(3) 工作线程的例程 (worker_routine)

工作线程的核心逻辑是一个循环:

  1. 加锁
  2. 检查任务队列是否为空并且线程池是否未关闭。如果条件成立,则调用 pthread_cond_wait() 阻塞等待
  3. 检查是否需要关闭线程池或线程是否过多需要退出。
  4. 如果队列中有任务,则从队列中取出一个任务
  5. 解锁
  6. 执行任务
  7. 循环返回第 1 步。

:高等线程池的实现较为复杂,涉及动态线程管理(扩容/缩容)、任务拒绝策略、优雅关闭等高级特性,老师提供的代码框架展示了其核心思想。实际应用中,可以直接使用以下成熟的开源库

C 语言线程池库

对于纯 C 项目,以下库非常流行,它们通常轻量级且易于集成。

1. thpool (by Johan Astborg)

这是一个非常受欢迎且易于使用的 C 语言线程池库。它的代码简洁,文档清晰,非常适合初学者和中小型项目。

  • 主要特点:

    • 纯 C 实现:无任何外部依赖,只需引入 thpool.cthpool.h 即可使用。
    • API 简单直观:创建、添加任务、销毁等操作都非常简单。
    • 任务队列:内置了任务队列和同步机制(互斥锁、信号量)。
    • 动态调整:支持在运行时增加或减少线程数量。
    • 任务优先级:支持为任务设置优先级。
  • 适用场景

    • 任何需要并发处理的 C 语言项目。
    • 希望快速集成一个稳定线程池而不愿深入复杂细节的开发者。
    • 嵌入式 Linux 项目。
  • 基本用法示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include "thpool.h"

    void my_task(void* arg){
    printf("Thread #%u is executing task %d\n", (int)pthread_self(), *(int*)arg);
    }

    int main(){
    threadpool thpool = thpool_init(4); // 创建一个有4个线程的线程池
    int i;
    for (i=0; i<20; i++){
    int *a = malloc(sizeof(int));
    *a = i;
    thpool_add_work(thpool, my_task, a); // 添加任务
    };
    thpool_wait(thpool); // 等待所有任务完成
    thpool_destroy(thpool); // 销毁线程池
    return 0;
    }

C++ 语言线程池库

得益于 C11 及之后版本的语言特性(如 std::function, std::future, lambda 表达式),C 的线程池库通常功能更强大、使用更灵活。

1. BS::thread_pool (by Barak Shoshany)

这是一个现代化、轻量级且功能极其丰富的 C++17 线程池库,在 GitHub 上非常受欢迎。

  • 主要特点:

    • 仅头文件:只需包含 thread_pool.hpp 即可,集成极其方便。
    • 现代 C++ 设计:完美支持 lambda 表达式、std::function 等。
    • 支持 future:可以轻松获取任务的返回值,实现异步编程。
    • 高性能:代码经过优化,性能表现优异。
    • 任务同步:提供了等待任务完成的多种方式。
    • 动态调整:可以在运行时修改线程数量。
  • 适用场景:

    • 所有使用现代 C++ (C++17/20) 的项目。
    • 需要从并发任务中获取返回值的场景。
    • 追求高性能和易用性的项目。
  • 基本用法示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include "thread_pool.hpp"
    #include <iostream>

    int main() {
    BS::thread_pool pool(4); // 创建一个有4个线程的线程池

    // 提交任务并获取 future
    auto future1 = pool.submit([]{
    return 10 + 20;
    });

    pool.submit([]{
    std::cout << "This is a task without return value." << std::endl;
    });

    // 等待并获取任务返回值
    std::cout << "Result of task 1: " << future1.get() << std::endl;

    return 0;
    }

2. ThreadPool (by progschj)

这是一个非常经典和简洁的 C++11 线程池实现,同样是仅头文件。它的代码量很小,非常适合学习线程池的内部实现原理。

  • 主要特点:

    • 仅头文件:只需一个 ThreadPool.h 文件。
    • C++11 实现:兼容性好,依赖标准库。
    • 支持 future:同样支持提交任务并获取返回值。
    • 简洁明了:代码实现非常清晰,是学习的绝佳范本。
  • 适用场景:

    • 中小型 C++11/14 项目。
    • 希望理解线程池底层实现的开发者。
  • 基本用法示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include "ThreadPool.h"
    #include <iostream>

    int main() {
    ThreadPool pool(4); // 创建一个有4个线程的线程池

    // 提交任务并获取 future
    auto result = pool.enqueue([](int answer) {
    return answer;
    }, 42);

    // 等待并获取返回值
    std::cout << "Task returned: " << result.get() << std::endl;

    return 0;
    }

如何选择?

库名称语言主要优点适合场景
thpoolC纯 C、API 简单、支持动态调整C 项目、嵌入式 Linux、快速集成
BS::thread_poolC++功能最丰富、性能高、现代 C++ 特性支持好C++17/20 项目、需要异步获取结果、追求高性能
ThreadPool (progschj)C++简洁、经典、易于学习其实现原理C++11/14 项目、学习目的、中小型项目

总结建议

  • 如果你的项目是纯 C,或者你在嵌入式 Linux 环境下开发,thpool 是一个非常优秀和稳妥的选择。
  • 如果你的项目使用现代 C++ (C++17 或更高版本),强烈推荐 BS::thread_pool,它的功能、性能和易用性都是顶级的。
  • 如果你在使用 C++11/14,并且想要一个简单可靠的方案,或者想通过阅读源码来学习,ThreadPool (by progschj) 是一个绝佳的起点。

考试考点总结:Linux 并发编程

I. Linux 多线程编程 (Pthreads)

1. 线程概念与基础

  • 定义区分
    • 进程 (Process):是系统分配资源的最小单位,拥有独立的虚拟内存空间。
    • 线程 (Thread):是系统调度的最小单位,不分配新的内存空间,但在进程内参与调度。
  • 线程库:Linux 多线程编程主要依赖 POSIX Threads (Pthreads) 库。
  • 编译注意:由于 Pthreads 并非 C 标准库的一部分,编译链接时必须显式添加 -pthread 标志。

2. 核心生命周期函数(创建、等待与退出)

函数作用关键点
pthread_create创建新的线程执行流。传入 start_routine (新线程要执行的任务函数) 和 attr (线程属性)。
pthread_join阻塞地等待指定线程结束,并回收其占用的系统资源。重要性:线程结束后资源不会自动释放,如不回收会形成僵尸线程,导致资源泄漏。
pthread_exit显式安全地终止当前线程效果等同于在线程函数中 return value
exit(0)切勿使用。它会立即终止整个进程,导致进程内所有线程全部被强制销毁。

3. 线程资源管理(属性对象 pthread_attr_t

线程属性主要用于管理资源分配(栈大小)和资源回收(分离状态)。

  • 通用五步法:定义 -> pthread_attr_init() -> pthread_attr_set*() -> pthread_create() -> pthread_attr_destroy()
  • 栈大小:通过 pthread_attr_setstacksize() 控制线程内存消耗。若栈溢出将导致程序崩溃(段错误)。
  • 回收策略 (分离状态)
    • PTHREAD_CREATE_JOINABLE (默认):需要主程序调用 pthread_join 手动回收。
    • PTHREAD_CREATE_DETACHED (分离):系统自动回收资源,主程序无需 join
    • 可以通过 pthread_attr_setdetachstate() 在创建时设置,或在创建后调用 pthread_detach() 设置。

4. 线程同步与互斥(线程安全)

核心概念

  • 线程安全:代码在多线程调用下仍能保证正确性。
  • 竞态条件:多线程并发访问共享数据时,结果依赖于执行时序,是线程不安全的根源。
  • 临界区:访问共享资源的代码段,必须确保独占访问。
同步机制目的关键 API 与规则
互斥锁 (Mutex)独占访问临界区。pthread_mutex_lock() (阻塞等待加锁),pthread_mutex_unlock() (释放锁)。
读写锁 (RW Lock)提高“读多写少”场景的并发性。读锁可共享 (rdlock),写锁独占 (wrlock)。
信号量 (Semaphore)计数器,用于管理有限资源的访问或协调执行顺序。P 操作 (sem_wait):计数器减 1,值为 0 时阻塞。V 操作 (sem_post):计数器加 1,唤醒等待线程。
条件变量 (Cond Var)高效的等待/通知机制,避免忙等。必须与互斥锁配合使用pthread_cond_wait() 原子地解锁互斥锁并阻塞,被唤醒时自动重新加锁

II. Linux 进程基础与管理

1. 进程状态与生命周期

  • 进程:已经被加载到内存中的程序文件,是动态的。
  • 重点状态
    • 就绪态:等待 CPU 时间片。
    • 僵尸态 (Zombie):子进程死亡后,等待父进程调用 wait/waitpid 收尸。必须避免,否则会耗尽系统 PID。
    • 死亡态:父进程回收资源后,彻底清理。

2. 进程创建与回收

  • 创建 (fork)
    • 创建后父子进程执行同一段代码。
    • 不同点:PID 和 fork() 返回值不同(父进程返回子 PID,子进程返回 0)。
    • 资源:父子进程的全局变量、局部变量等各自独立修改,互不影响(资源独立)。
  • 回收 (wait/waitpid)
    • wait()阻塞等待任意子进程结束。
    • waitpid():更灵活,可等待特定子进程(pid > 0),可设置为非阻塞 (WNOHANG)。
    • 退出状态宏:使用 WIFEXITED() 判断是否正常退出,使用 WEXITSTATUS() 获取退出码。
  • 退出方式
    • exit(status):终止整个进程,执行清理操作(如刷新 I/O 缓冲区,调用 atexit 注册函数)。
    • _exit(status):立即终止进程,不执行任何清理操作。推荐在 fork() 后的子进程中使用,以避免影响父进程的 I/O 流。

3. 特殊进程关系

  • 僵尸进程:子进程先结束,父进程未回收。危害是占用 PID,导致系统瘫痪。
  • 孤儿进程:父进程先于子进程结束。孤儿进程会被系统的 1 号进程(systemd收养,由其负责回收。
  • 守护进程 (Daemon):脱离控制终端的后台进程。创建核心步骤包括 fork 两次(通常简化为一次,父进程退出),以及调用 setsid() 创建新会话并脱离控制终端。

4. 程序加载

  • system():执行 shell 命令字符串,会创建子进程,并阻塞等待命令执行完毕。
  • exec 系列函数最核心的加载机制。不会创建新进程,而是用新程序替换当前进程的内存空间。调用成功后,程序不会返回。常见模式是 fork() + exec()

III. 进程间通信 (IPC)

核心挑战:进程隔离(每个进程有独立的虚拟内存空间)。IPC 机制依赖于内核空间开辟共享区域。

1. 管道 (Pipe)

  • 匿名管道:仅用于具有亲缘关系的进程(如父子进程)。
  • 命名管道 (FIFO):在文件系统中存在路径名,可用于任意不相关进程通信。

2. 信号 (Signal)

  • 特点异步通信机制,用于通知事件发生,不传输数据。类似于软件中断。
  • 重要信号SIGKILL (9) 和 SIGSTOP (19) 不可被捕捉或忽略SIGCHLD (17) 用于父进程异步处理子进程退出。
  • 编程:使用 signal() 设置信号处理方式。

3. System V IPC (共享内存、消息队列、信号量)

System V IPC 机制遵循相似的工作流程:ftok() 创建密钥 -> *get() 获取 ID -> *snd/*rcv/shmat/semop 操作 -> *ctl() 销毁。

机制特点速度与用途
共享内存 (Shared Memory)最快的 IPC 方式。将同一块物理内存映射到多进程虚拟地址空间,进程直接读写,无需内核数据拷贝。
消息队列 (Message Queue)内核中的消息链表。克服管道只能传输字节流的限制,适用于传输结构化消息。
信号量 (Semaphore)计数器。主要用于进程间的同步与互斥,保护共享内存等资源。

总结

Linux 并发编程的核心是围绕进程(Process)和线程(Thread)的生命周期管理、同步互斥以及通信机制这三大支柱展开的

首先,你必须清晰地区分进程和线程:进程是系统分配资源的独立单位,通过 fork() 创建,拥有独立的内存空间,父进程必须调用 wait()waitpid() 回收子进程资源以防僵尸进程,并通过 WIFEXITED() 等宏解析其退出状态;线程是系统调度的最小单位,通过 pthread_create() 创建,它们共享进程的大部分资源(如内存),因此执行开销远小于进程,需要通过 pthread_join() 回收资源,或通过 pthread_detach() 设置为分离状态让系统自动回收

其次,由于线程共享内存,线程安全是重中之重,其核心是互斥与同步

  • 互斥是为了保护临界区,防止竞态条件。最核心的工具是互斥锁(Mutex),其操作流程为 pthread_mutex_init() -> pthread_mutex_lock() -> 访问共享资源 -> pthread_mutex_unlock()。对于“读多写少”的场景,应使用读写锁(Read-Write Lock)pthread_rwlock_rdlock() / pthread_rwlock_wrlock())来提高并发性能
  • 同步是为了协调线程间的执行顺序。关键工具有两种:信号量(Semaphore),通过 sem_init() 初始化,使用 sem_wait()(P 操作,申请资源)和 sem_post()(V 操作,释放资源)来控制对有限资源的访问;条件变量(Condition Variable),它必须与互斥锁配合使用,通过 pthread_cond_wait() 让线程在某个条件不满足时原子性地解锁并挂起等待,再由另一线程在改变条件后通过 pthread_cond_signal()pthread_cond_broadcast() 发出通知将其唤醒

最后,由于进程内存相互隔离,它们必须依赖内核提供的进程间通信(IPC) 机制:

  • 简单的通信方式包括匿名管道pipe(),用于父子进程)和命名管道mkfifo(),用于任意进程)
  • 信号(Signal) 是一种异步通知机制,通过 kill() 发送,并使用 signal() 注册处理函数,常用于事件通知(如 SIGCHLD 信号可用于异步回收子进程)。
  • 功能最强大的 System V IPC 体系,它们都依赖 ftok() 生成的密钥来标识:共享内存(Shared Memory)最快的 IPC 方式(shmget() / shmat()),因为它避免了内核数据拷贝,但必须配合信号量等工具进行同步;消息队列(Message Queue)msgget() / msgsnd() / msgrcv())则适用于结构化的小数据块交换。所有 System V IPC 对象都需要通过 ipcs 命令查看并通过 ipcrm 命令或 msgctl/shmctl 函数手动销毁