一、多线程编程:并发控制与线程安全实战

1.1 本章学习目标与重点
💡 理解C语言多线程的核心概念(线程、进程、并发、并行),掌握多线程编程的优势与适用场景;
💡 熟练运用POSIX线程库(pthread)核心API,实现线程创建、退出、等待、取消等基础操作;
💡 掌握线程同步与互斥机制(互斥锁、条件变量、信号量),解决并发访问共享资源的竞态问题;
💡 理解线程安全的核心原则,规避死锁、活锁、优先级反转等常见并发问题;
💡 结合实战案例,实现多线程数据处理、生产者-消费者模型、线程池等工程常用功能,提升并发编程能力。
在多核处理器普及的今天,多线程编程已成为C语言开发的核心技能之一。通过多线程,程序可以同时执行多个任务,充分利用CPU资源,提升程序响应速度和处理效率。本章从多线程基础概念入手,系统讲解线程操作API、同步互斥机制、线程安全设计等核心知识点,帮助你从“单线程思维”转向“并发思维”,写出高效、安全的多线程C语言程序。
1.2 多线程核心概念解析
在学习具体编程技巧之前,我们需要先理清多线程的核心概念,明确线程与进程的差异、并发与并行的区别,这是理解后续内容的基础。
1.2.1 线程与进程的本质差异
1. 核心定义
- 进程:操作系统资源分配的基本单位,是程序运行的独立实例。每个进程拥有独立的内存空间(代码段、数据段、堆区、栈区)、文件描述符等资源,进程间切换开销较大。
- 线程:进程内的执行单元,是CPU调度的基本单位。一个进程可以包含多个线程,所有线程共享进程的内存空间和资源(如全局变量、文件描述符),线程间切换开销远小于进程。
2. 核心差异对比
| 对比维度 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 资源分配 | 独立资源空间(内存、文件描述符等) | 共享所属进程的资源空间 |
| 调度单位 | 操作系统调度的最小单位(资源分配单位) | CPU调度的最小单位(执行单元) |
| 切换开销 | 大(需切换地址空间、保存进程上下文) | 小(仅需保存线程上下文,共享进程资源) |
| 通信方式 | 复杂(管道、消息队列、共享内存等) | 简单(直接访问共享变量、全局数据) |
| 稳定性 | 高(一个进程崩溃不影响其他进程) | 低(一个线程崩溃可能导致整个进程崩溃) |
| 适用场景 | 独立程序运行(如浏览器、编辑器) | 并发执行多个任务(如服务器多客户端处理、数据并行计算) |
3. 示例理解:进程与线程的关系
以“浏览器”为例:
- 打开一个浏览器,操作系统会创建一个浏览器进程,分配独立内存空间;
- 浏览器的“多个标签页”可视为多个线程,它们共享浏览器进程的资源(如网络连接、缓存数据);
- 关闭一个标签页(线程退出),不影响其他标签页和浏览器主进程;
- 若浏览器进程崩溃,所有标签页(线程)都会终止。
1.2.2 并发与并行的区别
- 并发(Concurrency):多个任务在同一时间段内交替执行,看似“同时进行”。例如,单核CPU上的多线程程序,CPU通过快速切换线程实现并发。
- 并行(Parallelism):多个任务在同一时刻同时执行,真正的“同时进行”。例如,多核CPU上的多线程程序,每个核心执行一个线程,实现并行。
✅ 结论:并发是“交替执行”,并行是“同时执行”;多线程编程既支持并发(单核CPU),也支持并行(多核CPU),核心价值是提升程序的执行效率和响应速度。
1.2.3 多线程编程的优势与风险
1. 核心优势
- 充分利用多核CPU资源:多核环境下,多线程可并行执行,大幅提升CPU利用率;
- 提升程序响应速度:耗时任务(如网络IO、文件读写)可放在后台线程执行,主线程保持响应;
- 简化复杂任务编程:将复杂任务拆分为多个独立子任务,每个线程负责一个子任务,降低编程复杂度;
- 减少资源消耗:相比多进程,线程共享资源,创建和切换开销小,可创建更多线程处理任务。
2. 潜在风险
- 竞态条件(Race Condition):多个线程并发访问共享资源(如全局变量),导致数据不一致;
- 死锁(Deadlock):多个线程互相等待对方释放资源,导致所有线程阻塞;
- 线程安全问题:非线程安全的函数或数据结构在多线程环境下使用,导致逻辑错误;
- 调试难度高:多线程程序的执行顺序不确定,问题难以复现和排查。
💡 开发建议:多线程编程的核心是“扬长避短”——利用多线程提升效率,同时通过同步互斥机制规避并发风险。
1.2.4 C语言多线程编程的实现方式
C语言本身没有内置多线程支持,需依赖操作系统提供的线程库:
- POSIX线程库(pthread):适用于Linux、Unix、macOS等类Unix系统,是最常用的C语言多线程实现方式;
- Windows线程库:适用于Windows系统,提供
CreateThread等API,与POSIX线程库语法不同; - 跨平台线程库:如Boost.Thread、Qt Thread,封装了不同系统的线程API,支持跨平台开发。
本章重点讲解POSIX线程库(pthread),其API标准化程度高、应用广泛,是C语言多线程编程的必备技能。
1.3 POSIX线程库(pthread)核心API详解
POSIX线程库(简称pthread)提供了一套完整的多线程操作API,涵盖线程创建、退出、等待、同步互斥等功能。使用时需包含头文件<pthread.h>,编译时需链接线程库(GCC编译器添加-pthread选项)。
1.3.1 线程创建:pthread_create
1. 函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
2. 参数说明
thread:输出参数,用于存储新创建线程的ID(pthread_t类型,本质是无符号长整型);attr:线程属性(如栈大小、优先级),通常设为NULL,使用默认属性;start_routine:线程入口函数(函数指针),线程创建后会执行该函数,格式为void *func(void *arg);arg:传递给线程入口函数的参数,若需传递多个参数,可封装为结构体指针。
3. 返回值
- 成功:返回0;
- 失败:返回非0错误码(不同错误对应不同值,可通过
strerror函数获取错误描述)。
4. 用法示例:创建简单线程
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
// 线程入口函数:打印线程ID和传入的参数
void *thread_func(void *arg) {
// 获取线程ID(pthread_self()返回当前线程ID)
pthread_t tid = pthread_self();
char *msg = (char *)arg;
printf("线程ID:%lu,收到消息:%s\n", (unsigned long)tid, msg);
// 线程执行5秒后退出
sleep(5);
printf("线程ID:%lu,执行完毕,退出!\n", (unsigned long)tid);
// 线程退出时返回数据(可通过pthread_join获取)
return (void *)"线程执行成功!";
}
int main() {
pthread_t tid; // 存储线程ID
char *msg = "Hello, Pthread!";
int ret;
// 创建线程
ret = pthread_create(&tid, NULL, thread_func, (void *)msg);
if (ret != 0) {
fprintf(stderr, "创建线程失败:%s\n", strerror(ret));
return 1;
}
printf("主线程:成功创建线程,线程ID:%lu\n", (unsigned long)tid);
// 主线程等待子线程退出(避免主线程先退出导致子线程被终止)
void *thread_ret;
ret = pthread_join(tid, &thread_ret);
if (ret != 0) {
fprintf(stderr, "等待线程失败:%s\n", strerror(ret));
return 1;
}
printf("主线程:子线程退出,返回值:%s\n", (char *)thread_ret);
return 0;
}
5. 编译与运行(Linux系统)
# 编译:链接pthread库
gcc pthread_create_demo.c -o pthread_create_demo -pthread
# 运行
./pthread_create_demo
6. 运行结果
主线程:成功创建线程,线程ID:140703345581824
线程ID:140703345581824,收到消息:Hello, Pthread!
线程ID:140703345581824,执行完毕,退出!
主线程:子线程退出,返回值:线程执行成功!
⚠️ 注意事项:
- 主线程需等待子线程退出:若主线程不等待(未调用
pthread_join),主线程执行完毕后会终止进程,所有子线程也会被强制终止; - 线程入口函数返回值:返回值类型为
void *,若需返回复杂数据,可动态分配内存(需主线程释放,避免内存泄漏); - 参数传递注意事项:传递给线程的参数若为局部变量,需确保线程执行期间变量未被销毁(建议使用全局变量或动态分配内存)。
1.3.2 线程等待:pthread_join
1. 函数原型
int pthread_join(pthread_t thread, void **retval);
2. 功能
等待指定线程(thread)退出,阻塞当前线程(通常是主线程)直到目标线程退出。
3. 参数说明
thread:要等待的线程ID(由pthread_create返回);retval:输出参数,用于存储目标线程的退出返回值(即线程入口函数的返回值),若无需获取返回值,可设为NULL。
4. 返回值
- 成功:返回0;
- 失败:返回非0错误码(如目标线程不存在、已被等待过)。
5. 用法示例:等待多个线程退出
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
// 线程入口函数:执行指定时长后退出
void *thread_func(void *arg) {
int thread_num = *(int *)arg;
int sleep_sec = thread_num + 1; // 线程1睡眠1秒,线程2睡眠2秒...
printf("线程%d:开始执行,睡眠%d秒\n", thread_num, sleep_sec);
sleep(sleep_sec);
printf("线程%d:执行完毕,退出!\n", thread_num);
// 动态分配返回值(主线程需释放)
int *ret = (int *)malloc(sizeof(int));
*ret = thread_num * 10; // 线程1返回10,线程2返回20...
return (void *)ret;
}
int main() {
const int THREAD_COUNT = 3;
pthread_t tids[THREAD_COUNT];
int thread_nums[THREAD_COUNT];
int ret;
// 创建3个线程
for (int i = 0; i < THREAD_COUNT; i++) {
thread_nums[i] = i + 1;
ret = pthread_create(&tids[i], NULL, thread_func, &thread_nums[i]);
if (ret != 0) {
fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
return 1;
}
printf("主线程:创建线程%d,ID:%lu\n", i+1, (unsigned long)tids[i]);
}
// 等待所有线程退出,获取返回值
for (int i = 0; i < THREAD_COUNT; i++) {
void *thread_ret;
ret = pthread_join(tids[i], &thread_ret);
if (ret != 0) {
fprintf(stderr, "等待线程%d失败:%s\n", i+1, strerror(ret));
continue;
}
printf("主线程:线程%d退出,返回值:%d\n", i+1, *(int *)thread_ret);
free(thread_ret); // 释放线程返回值的动态内存
}
printf("主线程:所有子线程执行完毕,退出!\n");
return 0;
}
6. 运行结果
主线程:创建线程1,ID:140605501441792
线程1:开始执行,睡眠1秒
主线程:创建线程2,ID:140605493049088
线程2:开始执行,睡眠2秒
主线程:创建线程3,ID:140605484656384
线程3:开始执行,睡眠3秒
线程1:执行完毕,退出!
主线程:线程1退出,返回值:10
线程2:执行完毕,退出!
主线程:线程2退出,返回值:20
线程3:执行完毕,退出!
主线程:线程3退出,返回值:30
主线程:所有子线程执行完毕,退出!
💡 技巧:pthread_join是“阻塞等待”,若需非阻塞等待线程退出,可使用pthread_tryjoin_np(非标准API)或结合线程状态标记实现。
1.3.3 线程退出:pthread_exit
1. 函数原型
void pthread_exit(void *retval);
2. 功能
终止当前线程的执行,返回指定值(retval),该返回值可被pthread_join获取。
3. 与return的区别
return:从线程入口函数返回,终止线程执行,本质上会调用pthread_exit;pthread_exit:可在线程入口函数的任意位置调用(如子函数中),直接终止线程,更灵活。
4. 用法示例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 子函数:模拟任务执行,执行失败则退出线程
void task(int thread_num) {
printf("线程%d:执行子任务...\n", thread_num);
sleep(2);
// 模拟任务失败,退出线程
printf("线程%d:子任务执行失败,退出线程!\n", thread_num);
pthread_exit((void *)"任务失败");
}
// 线程入口函数
void *thread_func(void *arg) {
int thread_num = *(int *)arg;
printf("线程%d:开始执行\n", thread_num);
task(thread_num); // 调用子函数
// 以下代码不会执行(线程已在task中退出)
printf("线程%d:继续执行...\n", thread_num);
return (void *)"任务成功";
}
int main() {
pthread_t tid;
int thread_num = 1;
int ret;
ret = pthread_create(&tid, NULL, thread_func, &thread_num);
if (ret != 0) {
fprintf(stderr, "创建线程失败:%s\n", strerror(ret));
return 1;
}
void *thread_ret;
ret = pthread_join(tid, &thread_ret);
if (ret != 0) {
fprintf(stderr, "等待线程失败:%s\n", strerror(ret));
return 1;
}
printf("主线程:线程退出,返回值:%s\n", (char *)thread_ret);
return 0;
}
5. 运行结果
线程1:开始执行
线程1:执行子任务...
线程1:子任务执行失败,退出线程!
主线程:线程退出,返回值:任务失败
1.3.4 线程取消:pthread_cancel
1. 函数原型
int pthread_cancel(pthread_t thread);
2. 功能
请求取消指定线程的执行,并非立即终止线程,而是向线程发送取消请求,线程在“取消点”响应请求。
3. 关键概念:取消点
线程检查是否有取消请求的位置,常见取消点包括:
- 系统调用(如
sleep、read、write); - 线程库函数(如
pthread_join、pthread_testcancel); - 手动调用
pthread_testcancel函数设置取消点。
4. 参数与返回值
thread:要取消的线程ID;- 成功:返回0;
- 失败:返回非0错误码。
5. 用法示例
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
// 线程入口函数:循环执行,定期检查取消点
void *thread_func(void *arg) {
int count = 0;
printf("线程:开始执行,定期检查取消请求...\n");
while (1) {
printf("线程:执行计数%d\n", count++);
sleep(1); // 系统调用,是取消点
// 手动设置取消点(可选)
pthread_testcancel();
}
// 不会执行(线程被取消)
return (void *)"线程正常退出";
}
int main() {
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, thread_func, NULL);
if (ret != 0) {
fprintf(stderr, "创建线程失败:%s\n", strerror(ret));
return 1;
}
// 主线程睡眠3秒后,取消子线程
sleep(3);
printf("主线程:发送取消请求...\n");
ret = pthread_cancel(tid);
if (ret != 0) {
fprintf(stderr, "取消线程失败:%s\n", strerror(ret));
return 1;
}
// 等待线程退出,获取返回值(被取消的线程返回PTHREAD_CANCELED)
void *thread_ret;
ret = pthread_join(tid, &thread_ret);
if (ret != 0) {
fprintf(stderr, "等待线程失败:%s\n", strerror(ret));
return 1;
}
if (thread_ret == PTHREAD_CANCELED) {
printf("主线程:线程被成功取消!\n");
} else {
printf("主线程:线程正常退出,返回值:%s\n", (char *)thread_ret);
}
return 0;
}
6. 运行结果
线程:开始执行,定期检查取消请求...
线程:执行计数0
线程:执行计数1
线程:执行计数2
主线程:发送取消请求...
主线程:线程被成功取消!
⚠️ 注意事项:
- 线程取消是“协作式”的,需线程到达取消点才能响应;
- 被取消的线程返回值为
PTHREAD_CANCELED(宏定义,值为(void *) -1); - 若线程持有资源(如锁、动态内存),被取消前需释放资源,可通过“线程清理函数”实现。
1.3.5 线程清理函数:pthread_cleanup_push/pthread_cleanup_pop
1. 函数原型
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
2. 功能
pthread_cleanup_push:注册线程清理函数(routine),线程退出时(正常退出、被取消)会执行该函数,释放资源;pthread_cleanup_pop:取消或执行清理函数,execute为1时执行清理函数,为0时不执行。
3. 用法示例:线程被取消时释放资源
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
// 线程清理函数:释放动态内存
void cleanup_func(void *arg) {
int *ptr = (int *)arg;
printf("清理函数:释放动态内存,地址:%p\n", ptr);
free(ptr);
}
// 线程入口函数
void *thread_func(void *arg) {
// 动态分配内存
int *data = (int *)malloc(sizeof(int));
*data = 100;
// 注册清理函数(必须与pthread_cleanup_pop成对出现)
pthread_cleanup_push(cleanup_func, data);
printf("线程:开始执行,数据:%d,内存地址:%p\n", *data, data);
while (1) {
sleep(1);
pthread_testcancel(); // 取消点
}
// 取消清理函数(execute=0,不执行)
pthread_cleanup_pop(0);
return NULL;
}
int main() {
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, thread_func, NULL);
if (ret != 0) {
fprintf(stderr, "创建线程失败:%s\n", strerror(ret));
return 1;
}
// 3秒后取消线程
sleep(3);
printf("主线程:发送取消请求...\n");
ret = pthread_cancel(tid);
if (ret != 0) {
fprintf(stderr, "取消线程失败:%s\n", strerror(ret));
return 1;
}
// 等待线程退出
ret = pthread_join(tid, NULL);
if (ret != 0) {
fprintf(stderr, "等待线程失败:%s\n", strerror(ret));
return 1;
}
printf("主线程:线程退出,资源已释放!\n");
return 0;
}
4. 运行结果
线程:开始执行,数据:100,内存地址:0x55f8d7a742a0
主线程:发送取消请求...
清理函数:释放动态内存,地址:0x55f8d7a742a0
主线程:线程退出,资源已释放!
💡 技巧:pthread_cleanup_push和pthread_cleanup_pop必须成对出现,且需在同一代码块中,否则会导致编译错误。
1.4 线程同步与互斥:解决竞态条件
多线程并发访问共享资源(如全局变量、静态变量、文件)时,若缺乏同步机制,会导致“竞态条件”——多个线程同时读写共享资源,导致数据不一致。线程同步与互斥是解决竞态条件的核心手段,常用机制包括互斥锁、条件变量、信号量。
1.4.1 竞态条件示例:无同步机制的共享资源访问
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define THREAD_COUNT 2
#define LOOP_COUNT 1000000
// 共享变量:计数器
int g_count = 0;
// 线程入口函数:对共享变量执行100万次自增
void *thread_incr(void *arg) {
for (int i = 0; i < LOOP_COUNT; i++) {
g_count++; // 共享资源访问:读g_count → 加1 → 写回g_count
}
printf("线程%d:执行完毕,g_count = %d\n", *(int *)arg, g_count);
return NULL;
}
int main() {
pthread_t tids[THREAD_COUNT];
int thread_nums[THREAD_COUNT];
int ret;
// 创建2个线程,同时自增共享变量
for (int i = 0; i < THREAD_COUNT; i++) {
thread_nums[i] = i + 1;
ret = pthread_create(&tids[i], NULL, thread_incr, &thread_nums[i]);
if (ret != 0) {
fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
return 1;
}
}
// 等待所有线程退出
for (int i = 0; i < THREAD_COUNT; i++) {
pthread_join(tids[i], NULL);
}
printf("主线程:所有线程执行完毕,最终g_count = %d\n", g_count);
return 0;
}
运行结果(预期2000000,实际结果不确定)
线程1:执行完毕,g_count = 1234567
线程2:执行完毕,g_count = 1897654
主线程:所有线程执行完毕,最终g_count = 1897654
问题分析
g_count++看似是一条语句,实际编译后会分解为三条机器指令:
- 从内存读取
g_count的值到CPU寄存器; - 寄存器中的值加1;
- 将寄存器中的值写回内存。
两个线程并发执行时,指令可能交叉执行,导致数据丢失:
- 线程1读取
g_count=100→ 线程2读取g_count=100→ 线程1加1→101 → 线程2加1→101 → 写回内存,最终g_count=101(预期102)。
✅ 结论:多个线程并发读写共享资源时,必须通过同步互斥机制保证“原子操作”(不可分割的操作),避免竞态条件。
1.4.2 互斥锁(Mutex):最常用的同步机制
互斥锁(Mutual Exclusion Lock)是最常用的线程同步机制,核心功能是“保证同一时刻只有一个线程能持有锁,从而独占访问共享资源”。
1. 互斥锁的核心操作
- 初始化:创建互斥锁,设置锁的属性;
- 加锁(P操作):线程尝试获取锁,若锁未被持有则成功获取,若锁已被持有则线程阻塞;
- 解锁(V操作):线程释放锁,唤醒等待该锁的线程;
- 销毁:释放互斥锁占用的资源。
2. 互斥锁核心API
| 功能 | 函数原型 | 说明 |
|---|---|---|
| 初始化 | 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); | 销毁互斥锁,释放资源 |
3. 用法示例:用互斥锁解决竞态条件
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define THREAD_COUNT 2
#define LOOP_COUNT 1000000
// 共享变量
int g_count = 0;
// 互斥锁
pthread_mutex_t g_mutex;
// 线程入口函数:加锁后自增共享变量
void *thread_incr(void *arg) {
int thread_num = *(int *)arg;
for (int i = 0; i < LOOP_COUNT; i++) {
// 加锁:保证g_count++是原子操作
pthread_mutex_lock(&g_mutex);
g_count++;
// 解锁:释放锁,允许其他线程访问
pthread_mutex_unlock(&g_mutex);
}
printf("线程%d:执行完毕,g_count = %d\n", thread_num, g_count);
return NULL;
}
int main() {
pthread_t tids[THREAD_COUNT];
int thread_nums[THREAD_COUNT];
int ret;
// 初始化互斥锁(默认属性)
ret = pthread_mutex_init(&g_mutex, NULL);
if (ret != 0) {
fprintf(stderr, "初始化互斥锁失败:%s\n", strerror(ret));
return 1;
}
// 创建线程
for (int i = 0; i < THREAD_COUNT; i++) {
thread_nums[i] = i + 1;
ret = pthread_create(&tids[i], NULL, thread_incr, &thread_nums[i]);
if (ret != 0) {
fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
return 1;
}
}
// 等待线程退出
for (int i = 0; i < THREAD_COUNT; i++) {
pthread_join(tids[i], NULL);
}
printf("主线程:所有线程执行完毕,最终g_count = %d\n", g_count);
// 销毁互斥锁
pthread_mutex_destroy(&g_mutex);
return 0;
}
4. 运行结果(预期2000000)
线程1:执行完毕,g_count = 1987654
线程2:执行完毕,g_count = 2000000
主线程:所有线程执行完毕,最终g_count = 2000000
5. 互斥锁的关键注意事项
⚠️ 注意1:加锁与解锁必须成对出现,避免死锁或资源泄漏。例如,线程加锁后未解锁就退出,会导致其他线程永久阻塞;
⚠️ 注意2:锁的粒度要适中。锁粒度太粗(如整个函数加锁)会导致线程串行执行,失去多线程优势;锁粒度太细(如每条语句加锁)会增加锁操作开销;
⚠️ 注意3:避免在持有锁时调用耗时操作(如睡眠、IO),减少其他线程的等待时间;
⚠️ 注意4:互斥锁是“线程私有”的,仅持有锁的线程可解锁,其他线程解锁会导致错误。
1.4.3 条件变量(Condition Variable):线程间通信
条件变量用于线程间的“通信”,允许一个线程等待某个条件成立,另一个线程在条件成立时唤醒等待的线程。条件变量通常与互斥锁配合使用,解决“线程等待某个条件”的场景(如生产者-消费者模型)。
1. 条件变量的核心API
| 功能 | 函数原型 | 说明 |
|---|---|---|
| 初始化 | int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); | 初始化条件变量,attr=NULL使用默认属性 |
| 等待条件 | 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); | 销毁条件变量,释放资源 |
2. 核心原理
pthread_cond_wait调用时,会先释放互斥锁,再阻塞线程,避免持有锁导致其他线程无法修改条件;- 线程被唤醒后,会自动重新获取互斥锁,确保后续访问共享条件时的线程安全。
3. 用法示例:生产者-消费者模型(单生产者-单消费者)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#define BUFFER_SIZE 5 // 缓冲区大小
#define PRODUCT_COUNT 10 // 生产产品总数
// 缓冲区:存储产品(整数)
int g_buffer[BUFFER_SIZE];
// 缓冲区读写索引
int g_in = 0; // 写索引(生产者使用)
int g_out = 0; // 读索引(消费者使用)
// 产品数量
int g_product_num = 0;
// 互斥锁:保护缓冲区和产品数量
pthread_mutex_t g_mutex;
// 条件变量:缓冲区非空(消费者等待)、缓冲区非满(生产者等待)
pthread_cond_t g_not_empty;
pthread_cond_t g_not_full;
// 生产者线程:生产产品,放入缓冲区
void *producer(void *arg) {
int product_id = 0;
while (product_id < PRODUCT_COUNT) {
// 加锁:保护共享资源
pthread_mutex_lock(&g_mutex);
// 缓冲区满,等待“缓冲区非满”条件
while (g_product_num == BUFFER_SIZE) {
printf("生产者:缓冲区满,等待消费者消费...\n");
pthread_cond_wait(&g_not_full, &g_mutex);
}
// 生产产品,放入缓冲区
g_buffer[g_in] = ++product_id;
g_in = (g_in + 1) % BUFFER_SIZE;
g_product_num++;
printf("生产者:生产产品%d,缓冲区产品数:%d\n", product_id, g_product_num);
// 唤醒消费者:缓冲区非空
pthread_cond_signal(&g_not_empty);
// 解锁
pthread_mutex_unlock(&g_mutex);
// 模拟生产耗时
sleep(rand() % 2);
}
printf("生产者:完成所有产品生产,退出!\n");
return NULL;
}
// 消费者线程:从缓冲区取出产品,消费
void *consumer(void *arg) {
int product_id = 0;
while (1) {
// 加锁
pthread_mutex_lock(&g_mutex);
// 缓冲区空,等待“缓冲区非空”条件
while (g_product_num == 0) {
// 检查是否所有产品都已消费
if (g_product_num == 0 && product_id >= PRODUCT_COUNT) {
pthread_mutex_unlock(&g_mutex);
break;
}
printf("消费者:缓冲区空,等待生产者生产...\n");
pthread_cond_wait(&g_not_empty, &g_mutex);
}
// 检查是否退出
if (g_product_num == 0 && product_id >= PRODUCT_COUNT) {
break;
}
// 从缓冲区取出产品
product_id = g_buffer[g_out];
g_out = (g_out + 1) % BUFFER_SIZE;
g_product_num--;
printf("消费者:消费产品%d,缓冲区产品数:%d\n", product_id, g_product_num);
// 唤醒生产者:缓冲区非满
pthread_cond_signal(&g_not_full);
// 解锁
pthread_mutex_unlock(&g_mutex);
// 模拟消费耗时
sleep(rand() % 3);
}
printf("消费者:完成所有产品消费,退出!\n");
return NULL;
}
int main() {
pthread_t prod_tid, cons_tid;
int ret;
// 初始化互斥锁和条件变量
ret = pthread_mutex_init(&g_mutex, NULL);
ret |= pthread_cond_init(&g_not_empty, NULL);
ret |= pthread_cond_init(&g_not_full, NULL);
if (ret != 0) {
fprintf(stderr, "初始化同步机制失败:%s\n", strerror(ret));
return 1;
}
// 创建生产者和消费者线程
ret = pthread_create(&prod_tid, NULL, producer, NULL);
if (ret != 0) {
fprintf(stderr, "创建生产者线程失败:%s\n", strerror(ret));
return 1;
}
ret = pthread_create(&cons_tid, NULL, consumer, NULL);
if (ret != 0) {
fprintf(stderr, "创建消费者线程失败:%s\n", strerror(ret));
return 1;
}
// 等待线程退出
pthread_join(prod_tid, NULL);
// 生产者退出后,唤醒消费者,告知所有产品已生产
pthread_mutex_lock(&g_mutex);
pthread_cond_signal(&g_not_empty);
pthread_mutex_unlock(&g_mutex);
pthread_join(cons_tid, NULL);
// 销毁同步机制
pthread_mutex_destroy(&g_mutex);
pthread_cond_destroy(&g_not_empty);
pthread_cond_destroy(&g_not_full);
printf("主线程:所有线程执行完毕,退出!\n");
return 0;
}
4. 运行结果(节选)
生产者:生产产品1,缓冲区产品数:1
消费者:消费产品1,缓冲区产品数:0
生产者:生产产品2,缓冲区产品数:1
生产者:生产产品3,缓冲区产品数:2
消费者:消费产品2,缓冲区产品数:1
生产者:生产产品4,缓冲区产品数:2
生产者:生产产品5,缓冲区产品数:3
生产者:生产产品6,缓冲区产品数:4
生产者:生产产品7,缓冲区产品数:5
生产者:缓冲区满,等待消费者消费...
消费者:消费产品3,缓冲区产品数:4
生产者:生产产品8,缓冲区产品数:5
生产者:缓冲区满,等待消费者消费...
消费者:消费产品4,缓冲区产品数:4
生产者:生产产品9,缓冲区产品数:5
生产者:缓冲区满,等待消费者消费...
消费者:消费产品5,缓冲区产品数:4
生产者:生产产品10,缓冲区产品数:5
生产者:完成所有产品生产,退出!
消费者:消费产品6,缓冲区产品数:4
消费者:消费产品7,缓冲区产品数:3
消费者:消费产品8,缓冲区产品数:2
消费者:消费产品9,缓冲区产品数:1
消费者:消费产品10,缓冲区产品数:0
消费者:完成所有产品消费,退出!
主线程:所有线程执行完毕,退出!
💡 技巧:条件变量的等待必须用while循环,而非if判断。因为线程可能被“虚假唤醒”(无其他线程发送信号却被唤醒),while循环可重新检查条件,确保条件成立后再执行后续操作。
1.4.4 信号量(Semaphore):灵活的同步机制
信号量是比互斥锁更灵活的同步机制,不仅支持“互斥”(二值信号量),还支持“计数”(计数信号量),可用于限制并发访问的线程数量、实现生产者-消费者模型等场景。
1. 信号量的核心概念
- 信号量是一个非负整数,代表可用资源的数量;
- P操作(
sem_wait):信号量减1,若信号量为0则阻塞线程; - V操作(
sem_post):信号量加1,唤醒等待的线程。
2. 信号量核心API(POSIX信号量)
| 功能 | 函数原型 | 说明 |
|---|---|---|
| 初始化 | int sem_init(sem_t *sem, int pshared, unsigned int value); | 初始化信号量,pshared=0表示线程间共享,value为信号量初始值 |
| P操作 | int sem_wait(sem_t *sem); | 信号量减1,若为0则阻塞(阻塞型) |
| 尝试P操作 | int sem_trywait(sem_t *sem); | 信号量减1,若为0则返回错误(非阻塞型) |
| V操作 | int sem_post(sem_t *sem); | 信号量加1,唤醒等待的线程 |
| 获取信号量值 | int sem_getvalue(sem_t *sem, int *sval); | 获取当前信号量值,存入sval |
| 销毁 | int sem_destroy(sem_t *sem); | 销毁信号量,释放资源 |
3. 用法示例1:二值信号量(模拟互斥锁)
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define THREAD_COUNT 2
#define LOOP_COUNT 1000000
int g_count = 0;
// 二值信号量(初始值1,模拟互斥锁)
sem_t g_sem;
void *thread_incr(void *arg) {
int thread_num = *(int *)arg;
for (int i = 0; i < LOOP_COUNT; i++) {
sem_wait(&g_sem); // P操作:获取锁
g_count++;
sem_post(&g_sem); // V操作:释放锁
}
printf("线程%d:执行完毕,g_count = %d\n", thread_num, g_count);
return NULL;
}
int main() {
pthread_t tids[THREAD_COUNT];
int thread_nums[THREAD_COUNT];
int ret;
// 初始化二值信号量(初始值1)
ret = sem_init(&g_sem, 0, 1);
if (ret != 0) {
fprintf(stderr, "初始化信号量失败:%s\n", strerror(ret));
return 1;
}
for (int i = 0; i < THREAD_COUNT; i++) {
thread_nums[i] = i + 1;
ret = pthread_create(&tids[i], NULL, thread_incr, &thread_nums[i]);
if (ret != 0) {
fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
return 1;
}
}
for (int i = 0; i < THREAD_COUNT; i++) {
pthread_join(tids[i], NULL);
}
printf("主线程:最终g_count = %d\n", g_count);
sem_destroy(&g_sem);
return 0;
}
4. 用法示例2:计数信号量(限制并发线程数)
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <stdlib.h>
#define THREAD_COUNT 5 // 总线程数
#define MAX_CONCURRENT 2 // 最大并发线程数
// 计数信号量(初始值2,限制最多2个线程同时执行)
sem_t g_sem;
void *thread_func(void *arg) {
int thread_num = *(int *)arg;
// P操作:获取并发权限,若已达最大并发则阻塞
sem_wait(&g_sem);
printf("线程%d:获取并发权限,开始执行任务...\n", thread_num);
// 模拟任务耗时
sleep(rand() % 3);
printf("线程%d:任务执行完毕,释放并发权限\n", thread_num);
// V操作:释放并发权限
sem_post(&g_sem);
return NULL;
}
int main() {
pthread_t tids[THREAD_COUNT];
int thread_nums[THREAD_COUNT];
int ret;
// 初始化计数信号量(初始值2)
ret = sem_init(&g_sem, 0, MAX_CONCURRENT);
if (ret != 0) {
fprintf(stderr, "初始化信号量失败:%s\n", strerror(ret));
return 1;
}
// 创建5个线程
for (int i = 0; i < THREAD_COUNT; i++) {
thread_nums[i] = i + 1;
ret = pthread_create(&tids[i], NULL, thread_func, &thread_nums[i]);
if (ret != 0) {
fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
return 1;
}
}
// 等待所有线程退出
for (int i = 0; i < THREAD_COUNT; i++) {
pthread_join(tids[i], NULL);
}
sem_destroy(&g_sem);
printf("主线程:所有线程执行完毕!\n");
return 0;
}
5. 运行结果(节选)
线程1:获取并发权限,开始执行任务...
线程2:获取并发权限,开始执行任务...
线程1:任务执行完毕,释放并发权限
线程3:获取并发权限,开始执行任务...
线程2:任务执行完毕,释放并发权限
线程4:获取并发权限,开始执行任务...
线程3:任务执行完毕,释放并发权限
线程5:获取并发权限,开始执行任务...
线程4:任务执行完毕,释放并发权限
线程5:任务执行完毕,释放并发权限
主线程:所有线程执行完毕!
✅ 结论:信号量功能更灵活,二值信号量可替代互斥锁,计数信号量可限制并发线程数;但互斥锁更适合纯互斥场景,支持“递归加锁”“错误恢复”等特性,需根据场景选择。
1.5 线程安全与常见并发问题
1.5.1 线程安全的定义与判断标准
- 线程安全:多个线程并发访问某个函数、数据结构或资源时,不会出现数据不一致、逻辑错误等问题,且结果与单线程访问一致;
- 非线程安全:多个线程并发访问时,可能出现竞态条件、数据损坏等问题。
线程安全的判断标准
- 不使用全局变量、静态变量(或对其进行同步保护);
- 不依赖函数调用顺序或外部状态;
- 动态分配的内存由调用者管理,或内部进行线程安全的内存管理;
- 对共享资源的访问必须通过同步机制(锁、信号量等)进行保护。
常见线程安全的函数与非线程安全的函数
- 线程安全函数(如
strlen、memcpy):仅操作传入的参数,不使用全局/静态变量; - 非线程安全函数(如
strtok、rand):使用静态变量存储中间状态,多个线程并发调用会导致状态混乱。
💡 技巧:非线程安全函数的线程安全版本通常以_r结尾(如strtok_r是strtok的线程安全版本),使用时需传入线程私有变量存储中间状态。
1.5.2 常见并发问题与解决方案
1. 死锁(Deadlock)
(1)定义与产生条件
死锁是指多个线程互相等待对方释放资源,导致所有线程永久阻塞,无法继续执行。
死锁产生的四个必要条件(缺一不可):
- 互斥条件:资源只能被一个线程持有;
- 占有并等待条件:线程持有一个资源,同时等待另一个资源;
- 不可剥夺条件:资源只能被持有线程主动释放,无法强制剥夺;
- 循环等待条件:多个线程形成循环等待链(线程1等待线程2的资源,线程2等待线程1的资源)。
(2)死锁示例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 两个互斥锁
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;
// 线程1:先加锁a,再加锁b
void *thread1(void *arg) {
printf("线程1:尝试获取锁a...\n");
pthread_mutex_lock(&mutex_a);
printf("线程1:获取锁a成功,尝试获取锁b...\n");
sleep(1); // 模拟持有锁a时的耗时操作,让线程2有机会获取锁b
pthread_mutex_lock(&mutex_b);
printf("线程1:获取锁b成功,执行任务...\n");
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
return NULL;
}
// 线程2:先加锁b,再加锁a
void *thread2(void *arg) {
printf("线程2:尝试获取锁b...\n");
pthread_mutex_lock(&mutex_b);
printf("线程2:获取锁b成功,尝试获取锁a...\n");
sleep(1); // 模拟持有锁b时的耗时操作,让线程1有机会获取锁a
pthread_mutex_lock(&mutex_a);
printf("线程2:获取锁a成功,执行任务...\n");
pthread_mutex_unlock(&mutex_a);
pthread_mutex_unlock(&mutex_b);
return NULL;
}
int main() {
pthread_t tid1, tid2;
int ret;
pthread_mutex_init(&mutex_a, NULL);
pthread_mutex_init(&mutex_b, NULL);
pthread_create(&tid1, NULL, thread1, NULL);
pthread_create(&tid2, NULL, thread2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&mutex_a);
pthread_mutex_destroy(&mutex_b);
return 0;
}
(3)运行结果(死锁)
线程1:尝试获取锁a...
线程1:获取锁a成功,尝试获取锁b...
线程2:尝试获取锁b...
线程2:获取锁b成功,尝试获取锁a...
// 死锁,程序卡住,无法继续执行
(4)死锁解决方案
💡 方案1:破坏循环等待条件——统一锁的获取顺序。所有线程按相同顺序获取锁(如先锁a后锁b),避免循环等待;
💡 方案2:破坏占有并等待条件——一次性获取所有资源。线程启动时,一次性获取所需的所有锁,获取失败则释放已获取的锁;
💡 方案3:破坏不可剥夺条件——设置锁的超时时间。使用pthread_mutex_trylock或pthread_mutex_timedlock,超时未获取锁则释放已持有锁;
💡 方案4:破坏互斥条件——使用共享资源的并发访问机制。如读写锁、无锁数据结构,减少互斥锁的使用;
💡 方案5:死锁检测与恢复。定期检测线程状态,发现死锁时强制释放资源或终止部分线程。
2. 活锁(Livelock)
(1)定义
活锁是指多个线程不断修改自身状态以响应对方的状态变化,但始终无法推进任务,看似“活跃”实则“停滞”。例如,两个线程互相礼让资源,导致都无法获取资源。
(2)解决方案
- 引入随机延迟:线程重试前加入随机睡眠时长,打破对称等待;
- 固定优先级:为线程设置固定优先级,高优先级线程优先获取资源;
- 限制重试次数:线程重试次数达到阈值后,暂停或退出,避免无限循环。
3. 优先级反转(Priority Inversion)
(1)定义
低优先级线程持有高优先级线程所需的资源,导致高优先级线程阻塞,低优先级线程因被中优先级线程抢占而无法释放资源,最终高优先级线程的执行优先级低于中优先级线程。
(2)解决方案
- 优先级继承:低优先级线程持有高优先级线程所需的锁时,临时提升低优先级线程的优先级,使其能尽快释放锁;
- 优先级天花板:为锁设置优先级天花板(高于所有使用该锁的线程优先级),持有锁的线程优先级提升至天花板,避免被中优先级线程抢占;
- 避免长时间持有锁:低优先级线程持有锁时,尽量缩短执行时间,快速释放锁。
1.6 多线程实战案例
1.6.1 案例1:多线程并行计算(矩阵乘法)
需求描述
实现矩阵乘法的多线程并行计算,将矩阵A(M×N)与矩阵B(N×P)相乘,结果存储在矩阵C(M×P)中。通过多线程将每行的计算任务分配给不同线程,充分利用多核CPU资源,提升计算效率。
核心思路
- 定义矩阵数据结构,初始化输入矩阵A和B;
- 创建M个线程,每个线程负责计算结果矩阵C的一行;
- 线程通过参数获取矩阵数据、行号等信息,计算对应行的每个元素(矩阵乘法规则:C[i][j] = ΣA[i][k]×B[k][j],k=0~N-1);
- 主线程等待所有线程计算完成,输出结果矩阵。
代码实现
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// 矩阵维度定义
#define M 4 // 矩阵A的行数
#define N 4 // 矩阵A的列数 = 矩阵B的行数
#define P 4 // 矩阵B的列数
#define THREAD_COUNT M // 线程数 = 矩阵C的行数
// 输入矩阵A、B,输出矩阵C
int g_matrix_A[M][N];
int g_matrix_B[N][P];
int g_matrix_C[M][P];
// 线程参数结构体:传递矩阵数据和行号
typedef struct {
int row; // 负责计算的行号
} ThreadParam;
// 线程入口函数:计算矩阵C的指定行
void *matrix_multiply_row(void *arg) {
ThreadParam *param = (ThreadParam *)arg;
int row = param->row;
printf("线程%d:开始计算矩阵C的第%d行...\n", row+1, row+1);
// 计算该行的每个元素
for (int j = 0; j < P; j++) {
g_matrix_C[row][j] = 0;
for (int k = 0; k < N; k++) {
g_matrix_C[row][j] += g_matrix_A[row][k] * g_matrix_B[k][j];
}
}
printf("线程%d:完成矩阵C的第%d行计算\n", row+1, row+1);
return NULL;
}
// 初始化矩阵A和B(随机生成1~10的整数)
void init_matrices() {
srand(time(NULL));
printf("初始化矩阵A(%d×%d):\n", M, N);
for (int i = 0; i < M; i++) {
for (int j = 0; j < N; j++) {
g_matrix_A[i][j] = rand() % 10 + 1;
printf("%d\t", g_matrix_A[i][j]);
}
printf("\n");
}
printf("\n初始化矩阵B(%d×%d):\n", N, P);
for (int i = 0; i < N; i++) {
for (int j = 0; j < P; j++) {
g_matrix_B[i][j] = rand() % 10 + 1;
printf("%d\t", g_matrix_B[i][j]);
}
printf("\n");
}
}
// 打印矩阵C
void print_matrix_C() {
printf("\n矩阵乘法结果C(%d×%d):\n", M, P);
for (int i = 0; i < M; i++) {
for (int j = 0; j < P; j++) {
printf("%d\t", g_matrix_C[i][j]);
}
printf("\n");
}
}
int main() {
pthread_t tids[THREAD_COUNT];
ThreadParam params[THREAD_COUNT];
int ret;
// 初始化矩阵A和B
init_matrices();
// 记录开始时间
clock_t start_time = clock();
// 创建线程,每个线程计算一行
for (int i = 0; i < THREAD_COUNT; i++) {
params[i].row = i;
ret = pthread_create(&tids[i], NULL, matrix_multiply_row, ¶ms[i]);
if (ret != 0) {
fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
return 1;
}
}
// 等待所有线程计算完成
for (int i = 0; i < THREAD_COUNT; i++) {
pthread_join(tids[i], NULL);
}
// 记录结束时间,计算耗时
clock_t end_time = clock();
double cost_time = (double)(end_time - start_time) / CLOCKS_PER_SEC;
// 打印结果
print_matrix_C();
printf("\n多线程矩阵乘法完成,总耗时:%.4f秒\n", cost_time);
return 0;
}
运行结果(节选)
初始化矩阵A(4×4):
3 5 7 2
9 1 4 6
8 3 5 1
2 7 9 4
初始化矩阵B(4×4):
6 8 1 3
2 5 9 7
4 3 6 2
1 7 8 5
线程1:开始计算矩阵C的第1行...
线程2:开始计算矩阵C的第2行...
线程3:开始计算矩阵C的第3行...
线程4:开始计算矩阵C的第4行...
线程1:完成矩阵C的第1行计算
线程3:完成矩阵C的第3行计算
线程2:完成矩阵C的第2行计算
线程4:完成矩阵C的第4行计算
矩阵乘法结果C(4×4):
60 78 106 70
83 143 121 92
71 106 91 62
83 104 161 103
多线程矩阵乘法完成,总耗时:0.0001秒
代码解析
- 任务拆分:按行拆分矩阵乘法任务,每个线程独立计算一行,无共享资源竞争(矩阵A、B为只读,矩阵C的每行由单独线程写入),无需同步机制,效率最高;
- 并行优势:多核CPU上,4个线程并行计算,相比单线程效率提升接近4倍(矩阵越大,优势越明显);
- 扩展性:可根据CPU核心数调整线程数,如8核CPU可创建8个线程,处理更大规模矩阵。
1.6.2 案例2:线程池(Thread Pool)设计与实现
需求描述
实现一个通用线程池,支持:
- 初始化线程池时指定线程数量;
- 向线程池提交任务(函数指针+参数);
- 线程池中的线程循环获取任务并执行,执行完毕后继续等待新任务;
- 支持关闭线程池,等待所有任务执行完毕后退出。
核心思路
- 任务队列:用链表存储待执行的任务,每个任务包含函数指针和参数;
- 线程池结构体:包含线程数组、任务队列、互斥锁(保护任务队列)、条件变量(任务队列非空/非满)、线程池状态(运行/关闭);
- 线程函数:线程池中的线程循环等待任务,获取任务后执行,执行完毕后继续等待;
- 任务提交函数:将任务添加到任务队列,唤醒等待的线程;
- 线程池关闭函数:设置线程池状态为“关闭”,唤醒所有线程,等待线程退出后释放资源。
代码实现
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// 任务结构体:存储任务函数和参数
typedef struct Task {
void (*func)(void *); // 任务函数指针
void *arg; // 任务参数
struct Task *next; // 下一个任务(链表节点)
} Task;
// 线程池结构体
typedef struct ThreadPool {
pthread_t *threads; // 线程数组
int thread_count; // 线程数量
Task *task_queue; // 任务队列头
Task *task_tail; // 任务队列尾
int task_count; // 任务队列中任务数量
pthread_mutex_t mutex; // 保护任务队列的互斥锁
pthread_cond_t not_empty; // 任务队列非空条件变量
int is_shutdown; // 线程池状态:0=运行,1=关闭
} ThreadPool;
// 线程池全局实例(简化设计,实际可改为动态创建)
ThreadPool g_thread_pool;
// 线程函数:线程池中的线程循环执行任务
void *thread_pool_worker(void *arg) {
printf("线程%d:启动,等待任务...\n", *(int *)arg);
free(arg); // 释放线程编号的动态内存
while (1) {
// 加锁保护任务队列
pthread_mutex_lock(&g_thread_pool.mutex);
// 任务队列为空且线程池未关闭,等待任务
while (g_thread_pool.task_count == 0 && !g_thread_pool.is_shutdown) {
pthread_cond_wait(&g_thread_pool.not_empty, &g_thread_pool.mutex);
}
// 线程池已关闭且任务队列为空,退出线程
if (g_thread_pool.is_shutdown && g_thread_pool.task_count == 0) {
pthread_mutex_unlock(&g_thread_pool.mutex);
printf("线程%d:线程池关闭,退出!\n", pthread_self() % 1000);
pthread_exit(NULL);
}
// 从任务队列头部取出任务
Task *task = g_thread_pool.task_queue;
g_thread_pool.task_queue = task->next;
g_thread_pool.task_count--;
if (g_thread_pool.task_count == 0) {
g_thread_pool.task_tail = NULL;
}
// 解锁
pthread_mutex_unlock(&g_thread_pool.mutex);
// 执行任务
printf("线程%d:开始执行任务...\n", pthread_self() % 1000);
task->func(task->arg);
printf("线程%d:任务执行完毕!\n", pthread_self() % 1000);
// 释放任务内存
free(task);
}
}
// 初始化线程池
int thread_pool_init(int thread_count) {
memset(&g_thread_pool, 0, sizeof(ThreadPool));
// 初始化互斥锁和条件变量
int ret = pthread_mutex_init(&g_thread_pool.mutex, NULL);
ret |= pthread_cond_init(&g_thread_pool.not_empty, NULL);
if (ret != 0) {
fprintf(stderr, "初始化同步机制失败:%s\n", strerror(ret));
return -1;
}
// 设置线程数量,创建线程数组
g_thread_pool.thread_count = thread_count;
g_thread_pool.threads = (pthread_t *)malloc(thread_count * sizeof(pthread_t));
if (g_thread_pool.threads == NULL) {
fprintf(stderr, "分配线程数组内存失败!\n");
return -1;
}
// 创建线程
for (int i = 0; i < thread_count; i++) {
int *thread_num = (int *)malloc(sizeof(int));
*thread_num = i + 1;
ret = pthread_create(&g_thread_pool.threads[i], NULL, thread_pool_worker, thread_num);
if (ret != 0) {
fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
return -1;
}
}
printf("线程池初始化成功!线程数量:%d\n", thread_count);
return 0;
}
// 向线程池提交任务
int thread_pool_submit(void (*func)(void *), void *arg) {
// 创建任务
Task *task = (Task *)malloc(sizeof(Task));
if (task == NULL) {
fprintf(stderr, "创建任务失败!\n");
return -1;
}
task->func = func;
task->arg = arg;
task->next = NULL;
// 加锁,将任务添加到队列尾部
pthread_mutex_lock(&g_thread_pool.mutex);
if (g_thread_pool.task_tail == NULL) {
g_thread_pool.task_queue = task;
g_thread_pool.task_tail = task;
} else {
g_thread_pool.task_tail->next = task;
g_thread_pool.task_tail = task;
}
g_thread_pool.task_count++;
printf("提交任务成功!当前任务队列长度:%d\n", g_thread_pool.task_count);
// 唤醒等待的线程
pthread_cond_signal(&g_thread_pool.not_empty);
// 解锁
pthread_mutex_unlock(&g_thread_pool.mutex);
return 0;
}
// 关闭线程池:等待所有任务执行完毕后退出
void thread_pool_shutdown() {
// 加锁,设置线程池状态为关闭
pthread_mutex_lock(&g_thread_pool.mutex);
g_thread_pool.is_shutdown = 1;
pthread_mutex_unlock(&g_thread_pool.mutex);
printf("线程池开始关闭,唤醒所有线程...\n");
// 唤醒所有等待的线程
pthread_cond_broadcast(&g_thread_pool.not_empty);
// 等待所有线程退出
for (int i = 0; i < g_thread_pool.thread_count; i++) {
pthread_join(g_thread_pool.threads[i], NULL);
}
// 释放资源
free(g_thread_pool.threads);
pthread_mutex_destroy(&g_thread_pool.mutex);
pthread_cond_destroy(&g_thread_pool.not_empty);
printf("线程池已关闭!\n");
}
// 测试任务1:打印数字
void test_task1(void *arg) {
int num = *(int *)arg;
sleep(1); // 模拟任务耗时
printf("任务1:打印数字%d\n", num);
free(arg); // 释放任务参数内存
}
// 测试任务2:打印字符串
void test_task2(void *arg) {
char *str = (char *)arg;
sleep(2); // 模拟任务耗时
printf("任务2:打印字符串%s\n", str);
free(arg); // 释放任务参数内存
}
int main() {
// 初始化线程池(3个线程)
if (thread_pool_init(3) != 0) {
return 1;
}
// 提交5个任务
for (int i = 0; i < 3; i++) {
int *num = (int *)malloc(sizeof(int));
*num = i + 1;
thread_pool_submit(test_task1, num);
}
char *strs[] = {"Hello", "ThreadPool", "C语言"};
for (int i = 0; i < 2; i++) {
char *str = (char *)malloc(strlen(strs[i]) + 1);
strcpy(str, strs[i]);
thread_pool_submit(test_task2, str);
}
// 主线程睡眠5秒,等待任务执行
sleep(5);
// 关闭线程池
thread_pool_shutdown();
return 0;
}
运行结果(节选)
线程池初始化成功!线程数量:3
线程1:启动,等待任务...
线程2:启动,等待任务...
线程3:启动,等待任务...
提交任务成功!当前任务队列长度:1
提交任务成功!当前任务队列长度:2
提交任务成功!当前任务队列长度:3
提交任务成功!当前任务队列长度:4
提交任务成功!当前任务队列长度:5
线程1:开始执行任务...
线程2:开始执行任务...
线程3:开始执行任务...
任务1:打印数字1
线程1:任务执行完毕!
线程1:开始执行任务...
任务1:打印数字2
线程2:任务执行完毕!
线程2:开始执行任务...
任务1:打印数字3
线程3:任务执行完毕!
线程3:开始执行任务...
任务2:打印字符串Hello
线程1:任务执行完毕!
线程1:开始执行任务...
任务2:打印字符串ThreadPool
线程池开始关闭,唤醒所有线程...
线程2:任务执行完毕!
线程2:线程池关闭,退出!
线程3:任务执行完毕!
线程3:线程池关闭,退出!
线程1:任务执行完毕!
线程1:线程池关闭,退出!
线程池已关闭!
代码解析
- 任务队列设计:采用链表结构存储任务,支持动态添加任务,无需预设任务数量上限;
- 同步机制:互斥锁保护任务队列的读写操作,条件变量唤醒等待任务的线程,避免线程空轮询;
- 线程复用:线程池中的线程不会执行完一个任务就退出,而是循环等待新任务,减少线程创建/销毁的开销;
- 优雅关闭:关闭线程池时,先设置
is_shutdown标志,再唤醒所有线程,确保所有任务执行完毕后线程才退出,避免任务丢失; - 通用性:支持提交任意类型的任务(只需传入任务函数和参数),可适配不同业务场景(如网络请求处理、数据计算)。
线程池的进阶优化方向
- 任务队列长度限制:避免任务过多导致内存溢出,超过长度时拒绝提交或阻塞等待;
- 任务优先级:支持按优先级提交任务,高优先级任务优先执行;
- 线程动态扩容/缩容:根据任务队列长度自动增加或减少线程数量,平衡资源占用与执行效率;
- 任务执行结果回调:支持任务执行完毕后调用回调函数,返回执行结果;
- 异常处理:为任务执行添加异常捕获机制,避免单个任务崩溃导致整个线程退出。
1.6.3 案例3:读写锁实现高效并发访问
需求描述
实现一个支持高并发读写的共享数据结构(如缓存),要求:
- 多个读线程可同时访问共享数据(读-读不互斥);
- 写线程访问共享数据时,其他读线程和写线程需阻塞(写-读、写-写互斥);
- 读操作远多于写操作的场景下,提升并发效率(相比互斥锁,读写锁读并发更高)。
核心思路
- 读写锁(
pthread_rwlock_t):POSIX提供的读写锁API,支持读锁和写锁的分离; - 读锁(共享锁):多个线程可同时获取,适用于读操作;
- 写锁(排他锁):仅一个线程可获取,适用于写操作;
- 共享数据:模拟缓存,存储键值对(如用户ID与用户名的映射);
- 线程设计:创建多个读线程和少量写线程,模拟高并发读、低并发写的场景。
代码实现
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// 缓存大小
#define CACHE_SIZE 10
// 读线程数量(模拟高并发读)
#define READ_THREAD_COUNT 5
// 写线程数量(模拟低并发写)
#define WRITE_THREAD_COUNT 2
// 每个线程执行操作的次数
#define OP_COUNT 10
// 缓存结构体:键值对(用户ID-用户名)
typedef struct {
int user_id;
char username[20];
int is_valid; // 缓存是否有效:1=有效,0=无效
} CacheItem;
// 共享缓存数组
CacheItem g_cache[CACHE_SIZE];
// 读写锁:保护缓存访问
pthread_rwlock_t g_rwlock;
// 随机数种子(确保多线程下随机数不同)
pthread_mutex_t g_rand_mutex;
// 初始化缓存和同步机制
void init_cache() {
// 初始化缓存为无效状态
for (int i = 0; i < CACHE_SIZE; i++) {
g_cache[i].is_valid = 0;
}
// 初始化读写锁和随机数互斥锁
int ret = pthread_rwlock_init(&g_rwlock, NULL);
ret |= pthread_mutex_init(&g_rand_mutex, NULL);
if (ret != 0) {
fprintf(stderr, "初始化同步机制失败:%s\n", strerror(ret));
exit(1);
}
printf("缓存初始化完成!缓存大小:%d\n", CACHE_SIZE);
}
// 写操作:更新缓存(模拟添加/修改用户信息)
void write_cache(int user_id, const char *username) {
// 获取写锁(排他锁)
pthread_rwlock_wrlock(&g_rwlock);
// 查找缓存位置(简单哈希:user_id % CACHE_SIZE)
int idx = user_id % CACHE_SIZE;
g_cache[idx].user_id = user_id;
strncpy(g_cache[idx].username, username, sizeof(g_cache[idx].username)-1);
g_cache[idx].is_valid = 1;
printf("写线程:更新缓存,用户ID:%d,用户名:%s,缓存索引:%d\n",
user_id, username, idx);
// 释放写锁
pthread_rwlock_unlock(&g_rwlock);
// 模拟写操作耗时
usleep(rand() % 500000); // 0.1~0.5秒
}
// 读操作:查询缓存(根据用户ID获取用户名)
int read_cache(int user_id, char *username, int buf_len) {
// 获取读锁(共享锁)
pthread_rwlock_rdlock(&g_rwlock);
int ret = -1;
// 查找缓存
int idx = user_id % CACHE_SIZE;
if (g_cache[idx].is_valid && g_cache[idx].user_id == user_id) {
strncpy(username, g_cache[idx].username, buf_len-1);
username[buf_len-1] = '\0';
ret = 0; // 查找成功
printf("读线程:查询缓存成功,用户ID:%d,用户名:%s,缓存索引:%d\n",
user_id, username, idx);
} else {
printf("读线程:查询缓存失败,用户ID:%d(缓存无效或不存在)\n", user_id);
}
// 释放读锁
pthread_rwlock_unlock(&g_rwlock);
// 模拟读操作耗时
usleep(rand() % 100000); // 0.01~0.1秒
return ret;
}
// 写线程入口函数:随机生成用户ID和用户名,更新缓存
void *write_thread_func(void *arg) {
int thread_num = *(int *)arg;
free(arg);
for (int i = 0; i < OP_COUNT; i++) {
// 生成随机用户ID(1~100)
pthread_mutex_lock(&g_rand_mutex);
int user_id = rand() % 100 + 1;
pthread_mutex_unlock(&g_rand_mutex);
// 生成随机用户名(user_xxx)
char username[20];
snprintf(username, sizeof(username), "user_%d_%d", thread_num, user_id);
// 执行写操作
write_cache(user_id, username);
}
printf("写线程%d:执行完毕,退出!\n", thread_num);
return NULL;
}
// 读线程入口函数:随机查询用户ID对应的用户名
void *read_thread_func(void *arg) {
int thread_num = *(int *)arg;
free(arg);
char username[20];
for (int i = 0; i < OP_COUNT; i++) {
// 生成随机用户ID(1~100)
pthread_mutex_lock(&g_rand_mutex);
int user_id = rand() % 100 + 1;
pthread_mutex_unlock(&g_rand_mutex);
// 执行读操作
read_cache(user_id, username, sizeof(username));
}
printf("读线程%d:执行完毕,退出!\n", thread_num);
return NULL;
}
int main() {
pthread_t read_tids[READ_THREAD_COUNT];
pthread_t write_tids[WRITE_THREAD_COUNT];
int ret;
// 初始化随机数种子
srand(time(NULL));
// 初始化缓存和同步机制
init_cache();
// 记录开始时间
clock_t start_time = clock();
// 创建写线程
for (int i = 0; i < WRITE_THREAD_COUNT; i++) {
int *thread_num = (int *)malloc(sizeof(int));
*thread_num = i + 1;
ret = pthread_create(&write_tids[i], NULL, write_thread_func, thread_num);
if (ret != 0) {
fprintf(stderr, "创建写线程%d失败:%s\n", i+1, strerror(ret));
return 1;
}
}
// 创建读线程
for (int i = 0; i < READ_THREAD_COUNT; i++) {
int *thread_num = (int *)malloc(sizeof(int));
*thread_num = i + 1;
ret = pthread_create(&read_tids[i], NULL, read_thread_func, thread_num);
if (ret != 0) {
fprintf(stderr, "创建读线程%d失败:%s\n", i+1, strerror(ret));
return 1;
}
}
// 等待所有写线程退出
for (int i = 0; i < WRITE_THREAD_COUNT; i++) {
pthread_join(write_tids[i], NULL);
}
// 等待所有读线程退出
for (int i = 0; i < READ_THREAD_COUNT; i++) {
pthread_join(read_tids[i], NULL);
}
// 记录结束时间,计算耗时
clock_t end_time = clock();
double cost_time = (double)(end_time - start_time) / CLOCKS_PER_SEC;
// 销毁同步机制
pthread_rwlock_destroy(&g_rwlock);
pthread_mutex_destroy(&g_rand_mutex);
printf("\n所有线程执行完毕!总耗时:%.4f秒\n", cost_time);
printf("读线程数量:%d,写线程数量:%d,每个线程操作次数:%d\n",
READ_THREAD_COUNT, WRITE_THREAD_COUNT, OP_COUNT);
return 0;
}
运行结果(节选)
缓存初始化完成!缓存大小:10
写线程:更新缓存,用户ID:35,用户名:user_1_35,缓存索引:5
读线程:查询缓存失败,用户ID:78(缓存无效或不存在)
读线程:查询缓存失败,用户ID:23(缓存无效或不存在)
读线程:查询缓存成功,用户ID:35,用户名:user_1_35,缓存索引:5
读线程:查询缓存失败,用户ID:56(缓存无效或不存在)
写线程:更新缓存,用户ID:67,用户名:user_2_67,缓存索引:7
读线程:查询缓存成功,用户ID:67,用户名:user_2_67,缓存索引:7
读线程:查询缓存成功,用户ID:35,用户名:user_1_35,缓存索引:5
读线程:查询缓存成功,用户ID:67,用户名:user_2_67,缓存索引:7
...
写线程1:执行完毕,退出!
写线程2:执行完毕,退出!
读线程3:执行完毕,退出!
读线程1:执行完毕,退出!
读线程5:执行完毕,退出!
读线程2:执行完毕,退出!
读线程4:执行完毕,退出!
所有线程执行完毕!总耗时:1.8762秒
读线程数量:5,写线程数量:2,每个线程操作次数:10
代码解析
- 读写分离:读操作获取读锁(共享),多个读线程可同时访问;写操作获取写锁(排他),确保写操作的原子性,解决“读-写”“写-写”冲突;
- 高效并发:读操作远多于写操作时,读写锁的并发效率远高于互斥锁(互斥锁会导致所有读线程串行执行);
- 随机数线程安全:使用互斥锁保护
rand()函数调用(rand()是非线程安全的),避免多线程并发调用导致随机数生成异常; - 缓存设计:采用简单哈希映射(
user_id % CACHE_SIZE)定位缓存位置,模拟真实缓存的存储逻辑,易于扩展为更复杂的缓存策略(如LRU)。
1.7 多线程编程常见问题与避坑指南
1.7.1 线程安全函数与非线程安全函数混用
问题:在多线程环境中使用非线程安全函数(如strtok、rand),导致数据混乱或逻辑错误。
示例:
// 错误:strtok是非线程安全的,多个线程并发调用会覆盖静态中间状态
void *thread_func(void *arg) {
char str[] = "a,b,c,d";
char *token = strtok(str, ",");
while (token != NULL) {
printf("线程%d:token = %s\n", *(int *)arg, token);
token = strtok(NULL, ",");
}
return NULL;
}
避坑方案:
- 优先使用线程安全版本的函数(如
strtok_r替代strtok,rand_r替代rand); - 若必须使用非线程安全函数,需通过互斥锁保护,确保同一时刻只有一个线程调用;
- 线程安全函数列表可参考POSIX标准或编译器文档,避免凭经验判断。
1.7.2 共享资源未加同步保护
问题:多个线程并发读写共享资源(全局变量、静态变量、堆内存),未使用锁或信号量保护,导致竞态条件。
避坑方案:
- 明确共享资源:梳理程序中的共享资源,仅对必要的资源进行共享(减少同步开销);
- 最小化锁粒度:仅对共享资源的访问代码加锁,避免整个函数或大段代码加锁;
- 选择合适的同步机制:纯互斥场景用互斥锁,读多写少场景用读写锁,限制并发数用信号量。
1.7.3 线程退出时未释放资源
问题:线程退出时未释放持有的锁、动态内存、文件描述符等资源,导致资源泄漏或死锁。
示例:
// 错误:线程加锁后未解锁就退出,导致其他线程死锁
void *thread_func(void *arg) {
pthread_mutex_lock(&mutex);
// 模拟异常退出
if (some_error) {
return NULL; // 未解锁,导致死锁
}
pthread_mutex_unlock(&mutex);
return NULL;
}
避坑方案:
- 使用线程清理函数:通过
pthread_cleanup_push注册清理函数,释放锁、内存等资源; - 检查所有退出路径:确保线程在任何退出路径(正常返回、
pthread_exit、异常)都能释放资源; - 资源申请与释放成对出现:分配资源后立即规划释放时机,避免遗漏。
1.7.4 线程间传递局部变量指针
问题:将栈区的局部变量指针传递给其他线程,线程执行时局部变量已被销毁,导致野指针访问。
示例:
// 错误:传递局部变量指针给线程
void *thread_func(void *arg) {
int *num = (int *)arg;
printf("线程:num = %d\n", *num); // 局部变量已销毁,野指针访问
return NULL;
}
int main() {
pthread_t tid;
int local_num = 10;
pthread_create(&tid, NULL, thread_func, &local_num); // 传递局部变量指针
pthread_join(tid, NULL);
return 0;
}
避坑方案:
- 传递全局变量或静态变量的指针(生命周期为程序全程);
- 动态分配内存存储参数,线程执行完毕后释放(需确保线程已使用完参数);
- 若传递局部变量,需确保线程执行期间局部变量未被销毁(如主线程等待线程退出后再销毁局部变量)。
1.7.5 过度依赖线程优先级
问题:通过设置线程优先级控制执行顺序,导致优先级反转或程序行为不稳定。
避坑方案:
- 优先级仅用于提示调度器,不依赖优先级保证执行顺序(不同系统调度策略可能不同);
- 关键执行顺序通过同步机制(条件变量、信号量)控制,而非优先级;
- 若需设置优先级,需了解系统的调度策略(如Linux的SCHED_FIFO、SCHED_RR),避免设置无效优先级。
1.7.6 忽视线程创建失败处理
问题:调用pthread_create后未检查返回值,线程创建失败后程序继续执行,导致逻辑错误。
避坑方案:
- 所有线程创建、同步机制初始化等操作都必须检查返回值;
- 线程创建失败时,需释放已分配的资源,优雅退出程序或降级处理(如使用单线程执行);
- 使用
strerror函数输出错误描述,便于排查问题(如权限不足、资源耗尽)。
1.8 本章总结
本章系统讲解了C语言多线程编程的核心知识,核心内容包括:
- 多线程基础概念:明确了线程与进程的差异、并发与并行的区别,阐述了多线程编程的优势与潜在风险;
- POSIX线程库核心API:详细讲解了线程创建(
pthread_create)、等待(pthread_join)、退出(pthread_exit)、取消(pthread_cancel)等基础操作,以及线程清理函数的使用; - 线程同步与互斥机制:深入讲解了互斥锁(解决竞态条件)、条件变量(线程间通信)、信号量(灵活同步)、读写锁(读多写少场景优化)的原理与用法;
- 线程安全与并发问题:定义了线程安全的判断标准,分析了死锁、活锁、优先级反转等常见并发问题的产生原因与解决方案;
- 实战案例:实现了多线程矩阵乘法(并行计算)、通用线程池(线程复用)、读写锁缓存(高并发读写)三个工程常用案例,巩固了多线程编程技巧;
- 避坑指南:总结了线程安全函数混用、共享资源未保护、资源泄漏等常见问题,提供了具体的避坑方案。
多线程编程是C语言开发的高级技能,核心是“平衡并发效率与线程安全”。掌握本章内容后,你将能够充分利用多核CPU资源,实现高效的并发程序,应对服务器开发、嵌入式实时系统、数据并行计算等场景的需求。
下一章,我们将学习C语言的网络编程基础,包括TCP/UDP协议、Socket编程、客户端/服务器模型等核心知识点,实现网络数据传输功能。
929

被折叠的 条评论
为什么被折叠?



