本文是C/C++进阶开发的核心内容,聚焦**线程**相关的基础概念、创建与管理、同步互斥、通信方式及实战技巧,同时结合工业级编程规范讲解线程开发的最佳实践。内容覆盖POSIX线程(pthread)核心API、线程同步的四大方式(互斥锁、条件变量、信号量、读写锁)、线程安全与死锁规避,还包含线程池的设计思想与简单实现,适配Linux/UNIX平台的线程开发场景,是从基础语法走向工程化开发的必备知识点!
文章目录
一、线程基础概念(进程与线程的核心区别)
在学习线程开发前,必须先理解进程与线程的本质区别,明确线程的设计初衷和核心优势,这是掌握线程开发的基础。
1. 进程:操作系统资源分配的基本单位
- 定义:进程是程序的一次运行实例,是操作系统分配内存、CPU、文件描述符等系统资源的基本单位;
- 核心特性:进程拥有独立的地址空间(代码段、数据段、堆、栈),进程间资源相互隔离,互不干扰;
- 缺点:进程的创建、销毁、切换需要操作系统做大量的资源分配和回收工作,开销大、效率低,不适合高并发场景。
2. 线程:操作系统调度的基本单位
- 定义:线程是进程内的一条执行流,是操作系统CPU调度和执行的基本单位,也被称为轻量级进程(LWP);
- 核心特性:
- 同一进程内的所有线程共享进程的全部资源(地址空间、文件描述符、信号量、全局变量等);
- 每个线程拥有独立的栈空间和寄存器上下文,线程间切换仅需保存/恢复少量数据,开销小、效率高;
- 进程是资源容器,线程是容器内的执行流,一个进程可以包含多个线程(至少有一个主线程)。
3. 进程与线程的核心区别
| 资源分配 | 操作系统资源分配的基本单位 | 无独立资源,共享所属进程的资源 |
| 调度执行 | 非调度基本单位 | 操作系统CPU调度的基本单位 |
| 地址空间 | 拥有独立的虚拟地址空间 | 共享进程的虚拟地址空间 |
| 切换开销 | 开销大(需切换地址空间、资源等) | 开销小(仅切换栈和寄存器) |
| 通信方式 | 需借助进程间通信(管道、消息队列、共享内存等) | 直接访问共享变量(需同步互斥) |
| 稳定性 | 一个进程崩溃,不影响其他进程 | 一个线程崩溃,整个进程退出 |
4. 线程的核心优势与适用场景
核心优势
- 轻量级:创建/销毁/切换开销远小于进程,适合高并发场景;
- 通信高效:线程间共享进程资源,通信无需借助额外的IPC机制,效率更高;
- 资源利用率高:多线程可充分利用多核CPU的优势,实现并行执行。
适用场景
- CPU密集型任务:如数据计算、排序、加密解密,利用多线程实现多核并行,提升计算效率;
- IO密集型任务:如网络通信、文件读写、数据库操作,利用多线程掩盖IO等待时间,提升资源利用率;
- 高并发服务:如Web服务器、游戏服务器,通过多线程处理多个客户端的请求。
5. 主线程与子线程
- 主线程:进程启动时默认创建的线程,是程序的入口执行流,main()函数就是主线程的执行函数;
- 子线程:在主线程中通过线程API创建的线程,是进程内的额外执行流;
- 核心关系:主线程是子线程的创建者,默认情况下主线程退出,整个进程退出,所有子线程会被强制终止(可通过线程分离解决)。
二、POSIX线程(pthread)核心API(创建/退出/等待/分离)
C/C++本身没有提供原生的线程库,跨平台的线程开发主要依赖操作系统提供的线程库:
- Linux/UNIX平台:使用POSIX线程库(pthread),符合POSIX标准,是工业级开发的主流选择;
- Windows平台:使用Win32线程库(CreateThread等);
- 跨平台封装:可使用C++11的std::thread或Boost.Thread,底层封装了不同平台的线程API。
本文重点讲解Linux平台的pthread库,所有API均以pthread_为前缀,编译时需链接线程库(-lpthread)。
前置知识:pthread库的编译与使用
- 头文件:所有pthread API的声明均在<pthread.h>中;
- 编译命令:使用GCC编译时,必须通过-lpthread链接线程库,如gcc thread_demo.c -o thread_demo -lpthread;
- 线程ID:pthread库使用pthread_t类型表示线程ID,用于唯一标识一个线程,定义为typedef unsigned long int pthread_t;。
1. 线程创建:pthread_create()
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
功能
创建一个新的子线程,子线程从start_routine函数开始执行。
参数说明
- thread:传出参数,存储新创建线程的ID;
- attr:线程属性(如栈大小、分离状态),传NULL表示使用默认属性;
- start_routine:线程的执行函数,函数指针,返回值和参数均为void*(支持任意类型数据的传递);
- arg:传递给线程执行函数的参数,传NULL表示无参数。
返回值
- 成功:返回0;
- 失败:返回非0的错误码(pthread库的API不设置errno,直接返回错误码)。
核心注意点
- 线程执行函数的参数为void*,传递多个参数时需封装为结构体,通过指针传递;
- 传递局部变量时需注意生命周期,避免主线程先退出导致局部变量被释放,子线程访问野指针。
简单示例
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
// 线程执行函数
void* thread_func(void *arg)
{
char *msg = (char*)arg;
for (int i = 0; i < 3; i++)
{
printf("子线程:%s, i = %d\\n", msg, i);
sleep(1);
}
// 线程退出,返回NULL
pthread_exit(NULL);
}
int main()
{
pthread_t tid; // 存储子线程ID
// 创建子线程,传递参数"hello thread"
int ret = pthread_create(&tid, NULL, thread_func, (void*)"hello thread");
if (ret != 0)
{
printf("线程创建失败!错误码:%d\\n", ret);
return –1;
}
// 主线程休眠,避免主线程提前退出
sleep(5);
printf("主线程退出!\\n");
return 0;
}
2. 线程退出:pthread_exit()
#include <pthread.h>
void pthread_exit(void *retval);
功能
终止当前线程的执行,释放线程的资源,不会影响其他线程(区别于exit(),exit()会终止整个进程)。
参数
retval:线程的退出状态,是一个void*类型的指针,可被等待该线程的其他线程通过pthread_join()获取。
核心注意点
- 子线程中使用pthread_exit()退出,主线程中使用return退出即可(主线程调用pthread_exit()会终止自身,不影响子线程);
- 线程执行函数执行完毕后,会自动调用pthread_exit(),等价于return NULL;。
3. 线程等待:pthread_join()
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能
阻塞等待指定的子线程退出,并获取子线程的退出状态,同时回收子线程的资源(避免僵尸线程)。
参数说明
- thread:需要等待的子线程的ID;
- retval:传出参数,存储子线程的退出状态(即子线程pthread_exit()的参数),传NULL表示不获取退出状态。
返回值
- 成功:返回0;
- 失败:返回非0的错误码。
核心特性
- 阻塞性:调用pthread_join()的线程会被阻塞,直到目标子线程退出;
- 资源回收:若子线程退出后未被等待,会成为僵尸线程,占用系统资源,pthread_join()是回收子线程资源的核心方式;
- 一对一:一个pthread_join()只能等待一个子线程,等待多个子线程需循环调用。
示例:等待子线程退出并获取退出状态
void* thread_func(void *arg)
{
int num = *(int*)arg;
int sum = 0;
for (int i = 1; i <= num; i++) sum += i;
// 动态分配内存存储结果,避免局部变量生命周期问题
int *ret = (int*)malloc(sizeof(int));
*ret = sum;
pthread_exit((void*)ret);
}
int main()
{
pthread_t tid;
int n = 100;
pthread_create(&tid, NULL, thread_func, (void*)&n);
void *retval;
// 阻塞等待子线程退出,获取退出状态
pthread_join(tid, &retval);
printf("子线程计算结果:1-100的和 = %d\\n", *(int*)retval);
free(retval); // 释放子线程动态分配的内存
return 0;
}
4. 线程分离:pthread_detach()
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能
将指定的线程设置为分离状态,线程退出时会自动回收自身资源,无需其他线程调用pthread_join()等待。
参数
thread:需要设置为分离状态的线程ID(可传pthread_self()表示当前线程)。
返回值
- 成功:返回0;
- 失败:返回非0的错误码。
核心适用场景
- 主线程无需等待子线程退出,子线程的执行结果无需被其他线程获取;
- 避免主线程因调用pthread_join()被阻塞,提升程序执行效率;
- 防止子线程成为僵尸线程,节省系统资源。
注意点
- 线程设置为分离状态后,不能再调用pthread_join()等待,调用会返回错误;
- 可在创建线程时通过线程属性直接设置分离状态,替代pthread_detach()。
示例:子线程设置为分离状态
void* thread_func(void *arg)
{
// 将当前线程设置为分离状态
pthread_detach(pthread_self());
for (int i = 0; i < 3; i++)
{
printf("子线程执行:i = %d\\n", i);
sleep(1);
}
// 退出时自动回收资源
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
// 主线程无需等待,直接执行后续逻辑
printf("主线程继续执行,不阻塞!\\n");
// 主线程休眠,保证子线程执行完毕
sleep(5);
return 0;
}
5. 获取当前线程ID:pthread_self()
#include <pthread.h>
pthread_t pthread_self(void);
功能
获取当前线程的ID,无参数,返回值为当前线程的pthread_t类型ID。
示例
void* thread_func(void *arg)
{
printf("子线程ID:%lu\\n", (unsigned long)pthread_self());
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
printf("主线程ID:%lu\\n", (unsigned long)pthread_self());
printf("创建的子线程ID:%lu\\n", (unsigned long)tid);
pthread_join(tid, NULL);
return 0;
}
6. 线程取消:pthread_cancel()
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能
向指定的线程发送取消请求,请求该线程退出执行(并非立即终止,需线程到达取消点才会退出)。
核心注意点
- 取消点:线程执行过程中会检查是否有取消请求的位置,如sleep()、read()、write()、pthread_join()等系统调用;
- 线程可通过pthread_setcancelstate()设置取消状态(允许/禁止取消),通过pthread_setcanceltype()设置取消类型(立即取消/延迟取消);
- 取消线程后,需确保线程的资源被正确释放(如动态内存、文件描述符),可通过清理函数实现。
三、线程同步与互斥(解决资源竞争问题)
1. 问题根源:临界区与资源竞争
- 临界区(Critical Section):多个线程共享的资源(如全局变量、文件描述符、硬件设备),以及访问这些资源的代码段,称为临界区;
- 资源竞争(Race Condition):多个线程同时访问临界区,且至少有一个线程对资源进行写操作时,会导致资源的数据混乱、结果不可预期,这种现象称为资源竞争。
示例:多线程累加全局变量(资源竞争问题)
int g_sum = 0; // 全局变量(临界区)
void* add_func(void *arg)
{
for (int i = 0; i < 10000; i++)
{
g_sum++; // 临界区代码:对全局变量写操作
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, add_func, NULL);
pthread_create(&tid2, NULL, add_func, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("g_sum = %d\\n", g_sum); // 预期20000,实际结果小于20000
return 0;
}
问题原因:g_sum++并非原子操作,分为读取-修改-写入三步,两个线程交替执行时会覆盖彼此的修改,导致结果错误。
2. 核心解决方案:线程同步与互斥
线程同步与互斥的核心目标是保证多个线程对临界区的有序访问,避免资源竞争,确保程序执行结果的可预期性。
- 互斥(Mutual Exclusion):同一时间只允许一个线程访问临界区,其他线程需等待,实现“独占式”访问;
- 同步(Synchronization):协调多个线程的执行顺序,让线程按预定的顺序访问临界区,解决“生产-消费”等依赖问题。
pthread库提供了四种核心的同步互斥机制,分别适用于不同的场景:互斥锁、条件变量、信号量、读写锁。
3. 互斥锁(Mutex):最常用的互斥机制
核心思想
为临界区加一把“锁”,线程访问临界区前需获取锁,访问完成后释放锁;若锁已被其他线程持有,当前线程会被阻塞,直到锁被释放。
核心特性
- 唯一性:同一把锁同一时间只能被一个线程持有;
- 原子性:锁的获取和释放操作是原子操作,不会被中断;
- 非忙等:获取不到锁的线程会被阻塞,不会占用CPU资源。
互斥锁的核心API
#include <pthread.h>
// 1. 定义互斥锁(全局/静态,保证所有线程可见)
pthread_mutex_t mutex;
// 2. 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
// 静态初始化(推荐,简化代码)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 3. 获取互斥锁(加锁),阻塞式
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 尝试获取互斥锁,非阻塞式,获取不到直接返回错误
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 4. 释放互斥锁(解锁)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 5. 销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值
所有互斥锁API的返回值:成功返回0,失败返回非0错误码。
示例:用互斥锁解决全局变量累加的资源竞争问题
#include <pthread.h>
#include <stdio.h>
int g_sum = 0;
// 静态初始化互斥锁
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
void* add_func(void *arg)
{
for (int i = 0; i < 10000; i++)
{
pthread_mutex_lock(&g_mutex); // 加锁
g_sum++; // 临界区代码,唯一访问
pthread_mutex_unlock(&g_mutex); // 解锁
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, add_func, NULL);
pthread_create(&tid2, NULL, add_func, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&g_mutex); // 销毁锁
printf("g_sum = %d\\n", g_sum); // 正确结果:20000
return 0;
}
核心注意点
- 互斥锁必须全局/静态定义,保证所有访问临界区的线程都能看到;
- 加锁与解锁必须配对:加锁后必须解锁,否则会导致死锁;
- 临界区代码应尽可能简短:减少锁的持有时间,提升多线程的并发效率;
- 避免在临界区中调用阻塞函数(如sleep()、pthread_join()),否则会导致其他线程长时间等待。
4. 条件变量(Condition Variable):解决同步问题
核心思想
条件变量用于协调线程的执行顺序,让一个线程等待某个“条件”满足后再继续执行,另一个线程在条件满足时唤醒等待的线程。
核心特性
- 条件变量必须与互斥锁配合使用:用于保护条件变量的判断条件(避免竞态);
- 等待唤醒机制:等待的线程会释放互斥锁并进入阻塞状态,被唤醒后重新获取互斥锁并检查条件;
- 支持单播唤醒(唤醒一个等待线程)和广播唤醒(唤醒所有等待线程)。
条件变量的核心API
#include <pthread.h>
// 1. 定义条件变量(全局/静态)
pthread_cond_t cond;
// 2. 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
// 静态初始化(推荐)
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 3. 等待条件满足(释放互斥锁,阻塞等待)
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
// 限时等待,超时后自动返回
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
// 4. 唤醒一个等待的线程(单播)
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒所有等待的线程(广播)
int pthread_cond_broadcast(pthread_cond_t *cond);
// 5. 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
经典场景:生产者-消费者模型(单生产者单消费者)
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 1
int buf[BUF_SIZE]; // 缓冲区(临界区)
int buf_count = 0; // 缓冲区数据个数
// 初始化互斥锁和条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_prod = PTHREAD_COND_INITIALIZER; // 生产者条件
pthread_cond_t cond_cons = PTHREAD_COND_INITIALIZER; // 消费者条件
// 生产者线程:向缓冲区写数据
void* producer(void *arg)
{
for (int i = 0; i < 5; i++)
{
pthread_mutex_lock(&mutex);
// 缓冲区满,等待消费者消费
while (buf_count == BUF_SIZE)
{
pthread_cond_wait(&cond_prod, &mutex);
}
// 生产数据
buf[0] = i;
buf_count++;
printf("生产者生产:%d\\n", i);
// 唤醒消费者
pthread_cond_signal(&cond_cons);
pthread_mutex_unlock(&mutex);
sleep(1);
}
pthread_exit(NULL);
}
// 消费者线程:从缓冲区读数据
void* consumer(void *arg)
{
for (int i = 0; i < 5; i++)
{
pthread_mutex_lock(&mutex);
// 缓冲区空,等待生产者生产
while (buf_count == 0)
{
pthread_cond_wait(&cond_cons, &mutex);
}
// 消费数据
int data = buf[0];
buf_count—;
printf("消费者消费:%d\\n", data);
// 唤醒生产者
pthread_cond_signal(&cond_prod);
pthread_mutex_unlock(&mutex);
sleep(1);
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid_p, tid_c;
pthread_create(&tid_p, NULL, producer, NULL);
pthread_create(&tid_c, NULL, consumer, NULL);
pthread_join(tid_p, NULL);
pthread_join(tid_c, NULL);
// 销毁资源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond_prod);
pthread_cond_destroy(&cond_cons);
return 0;
}
核心注意点
- 条件变量的判断条件必须用while循环,而非if:防止虚假唤醒(线程被唤醒后,条件可能仍不满足);
- pthread_cond_wait()的第二个参数必须是已加锁的互斥锁,函数会自动释放锁并阻塞,被唤醒后重新获取锁;
- 生产/消费完成后,必须唤醒对应的线程,避免对方永久阻塞。
5. 信号量(Semaphore):通用的同步互斥机制
核心思想
信号量是一个整型计数器,通过P操作(减1)和V操作(加1)实现对临界区的访问控制,可实现互斥和同步,支持多个线程同时访问临界区(区别于互斥锁的独占式访问)。
分类
- 二值信号量:计数器值只能为0或1,等价于互斥锁,实现独占式访问;
- 计数信号量:计数器值可以为非负整数,允许N个线程同时访问临界区,实现有限并发。
信号量的核心API(System V/Posix,此处用Posix无名信号量)
#include <semaphore.h>
// 1. 定义信号量
sem_t sem;
// 2. 初始化信号量
// pshared:0表示线程间使用,非0表示进程间使用;value:信号量初始值
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 3. P操作:信号量减1,若值为0则阻塞
int sem_wait(sem_t *sem);
// 非阻塞P操作
int sem_trywait(sem_t *sem);
// 4. V操作:信号量加1,唤醒阻塞的线程
int sem_post(sem_t *sem);
// 5. 获取信号量当前值
int sem_getvalue(sem_t *sem, int *sval);
// 6. 销毁信号量
int sem_destroy(sem_t *sem);
返回值
成功返回0,失败返回-1并设置errno。
示例:用计数信号量实现生产者-消费者模型(多生产者多消费者)
信号量适合多生产者多消费者场景,无需配合互斥锁(底层已实现同步),代码更简洁。
6. 读写锁(Reader-Writer Lock):优化读多写少场景
核心思想
读写锁针对读多写少的场景做了优化,将访问分为读操作和写操作,遵循以下规则:
- 读共享:多个线程可以同时获取读锁,进行读操作,提高并发效率;
- 写独占:一个线程获取写锁后,其他线程无法获取读锁或写锁,实现独占式写操作;
- 读写互斥:一个线程获取读锁后,其他线程无法获取写锁,反之亦然。
核心特性
- 适合读多写少的场景(如配置文件读取、数据查询),比互斥锁具有更高的并发效率;
- 读锁为共享锁,写锁为排他锁。
读写锁的核心API
#include <pthread.h>
// 1. 定义读写锁
pthread_rwlock_t rwlock;
// 2. 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
// 静态初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 3. 获取读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 尝试获取读锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 4. 获取写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 尝试获取写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 5. 释放锁(读锁和写锁均用此函数释放)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 6. 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
适用场景
- 数据库查询系统:大量线程查询数据(读操作),少量线程更新数据(写操作);
- 配置文件管理:多个线程读取配置(读操作),偶尔有线程修改配置(写操作);
- 日志系统:多个线程写日志(写操作,需独占),少量线程读取日志(读操作)→ 不适合,写多读少场景用互斥锁。
四、线程间通信方式(数据交互的实现)
同一进程内的线程共享进程的全部资源,因此线程间通信比进程间通信更简单,核心分为基于共享资源的通信和基于消息传递的通信两大类,以下为常用的通信方式:
1. 共享变量(最直接的通信方式)
- 核心原理:多个线程访问进程的全局变量、静态变量(共享资源),通过同步互斥机制(互斥锁、条件变量)保证访问的有序性;
- 优点:实现简单、通信效率高,无需额外的API调用;
- 缺点:需手动处理同步互斥,容易出现资源竞争和死锁问题;
- 适用场景:线程间传递少量数据,且通信逻辑简单。
2. 共享内存(进阶的共享资源通信)
- 核心原理:通过mmap()函数创建一块共享内存区域,多个线程访问该区域实现数据交互(本质是共享资源的扩展);
- 优点:内存访问速度快,适合传递大量数据;
- 缺点:需配合同步互斥机制,实现稍复杂;
- 适用场景:线程间传递大量二进制数据(如文件数据、网络数据包)。
3. 管道(Pipe):基于消息传递的通信
- 核心原理:创建一个匿名管道,线程通过read()/write()函数从管道读/写数据,实现单向的消息传递;
- 优点:基于文件描述符,使用简单,无需手动处理同步(管道本身是阻塞的);
- 缺点:单向通信,双向通信需创建两个管道,适合传递少量字符/字节数据;
- 适用场景:线程间的简单消息通知(如状态码、指令)。
4. 消息队列(Message Queue):结构化的消息传递
- 核心原理:创建一个消息队列,线程通过msgsnd()/msgrcv()函数向队列发送/接收结构化的消息,实现异步通信;
- 优点:消息是结构化的,支持多类型消息,异步通信无需阻塞;
- 缺点:有系统调用开销,效率略低于共享变量;
- 适用场景:线程间的异步消息通信,且需要传递结构化数据。
5. 信号(Signal):简单的事件通知
- 核心原理:一个线程通过pthread_kill()向另一个线程发送信号,被发送信号的线程通过信号处理函数响应事件;
- 优点:实现简单,适合事件通知;
- 缺点:只能传递简单的信号码,无法传递复杂数据,信号处理函数的执行上下文有限;
- 适用场景:线程间的紧急事件通知(如线程退出、超时提醒)。
线程间通信方式对比与选型建议
| 共享变量 | 实现简单、效率最高 | 需处理同步互斥,易出问题 | 少量数据、简单通信 |
| 共享内存 | 速度快、适合大量数据 | 实现稍复杂,需同步 | 大量二进制数据传递 |
| 管道 | 使用简单、无需手动同步 | 单向通信、适合少量数据 | 简单消息通知 |
| 消息队列 | 结构化消息、异步通信 | 系统调用开销、效率一般 | 异步结构化消息通信 |
| 信号 | 实现简单、适合事件通知 | 无法传递复杂数据 | 紧急事件通知 |
选型核心原则:
- 优先使用共享变量:简单高效,满足大部分场景;
- 大量数据用共享内存:比消息队列/管道效率更高;
- 异步通信用消息队列:无需阻塞,支持结构化数据;
- 简单通知用信号/管道:实现成本低。
五、线程安全与死锁(规避开发中的核心问题)
1. 线程安全(Thread Safety)
定义
一个函数/数据结构在多个线程同时调用/访问时,无论线程的执行顺序如何,都能保证结果的正确性和数据的一致性,称为线程安全。
非线程安全的根源
- 访问共享资源时未做同步互斥处理,导致资源竞争;
- 函数内部使用静态/全局变量,且对其进行写操作;
- 函数的返回值为局部变量的指针,导致野指针。
实现线程安全的核心方法
常见的线程安全函数与非线程安全函数
- 线程安全函数:printf()(底层加锁)、pthread_*系列API、malloc()(底层加锁);
- 非线程安全函数:strtok()(使用静态缓冲区)、asctime()(使用静态缓冲区)、gethostbyname()(使用静态缓冲区)。
注:非线程安全函数通常有对应的线程安全版本,如strtok_r()(可重入版)、asctime_r()。
2. 死锁(Deadlock):线程开发的致命问题
定义
多个线程相互等待对方持有的锁资源,导致所有线程都被永久阻塞,无法继续执行,这种现象称为死锁。
死锁的四个必要条件(缺一不可)
经典死锁场景:两个线程互相等待对方的锁
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void* thread1(void *arg)
{
pthread_mutex_lock(&mutex1);
sleep(1); // 让thread2先获取mutex2
pthread_mutex_lock(&mutex2); // 等待thread2释放mutex2,阻塞
// … 临界区代码
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
pthread_exit(NULL);
}
void* thread2(void *arg)
{
pthread_mutex_lock(&mutex2);
sleep(1); // 让thread1先获取mutex1
pthread_mutex_lock(&mutex1); // 等待thread1释放mutex1,阻塞
// … 临界区代码
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
pthread_exit(NULL);
}
结果:thread1持有mutex1,等待mutex2;thread2持有mutex2,等待mutex1,形成循环等待,导致死锁。
死锁的规避与解决
(1)死锁的规避:破坏死锁的四个必要条件(推荐)
规避是最好的方式,在代码设计阶段就避免死锁的发生,核心方法:
示例:按固定顺序获取锁,规避死锁
// 规定锁的顺序:先获取mutex1,再获取mutex2
void* thread2(void *arg)
{
pthread_mutex_lock(&mutex1); // 按顺序获取
sleep(1);
pthread_mutex_lock(&mutex2);
// … 临界区代码
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
pthread_exit(NULL);
}
此时两个线程都按mutex1→mutex2的顺序获取锁,不会形成循环等待,避免死锁。
(2)死锁的解决:检测与恢复(适用于复杂场景)
- 死锁检测:通过监控线程的锁持有状态和等待状态,检测是否出现循环等待;
- 死锁恢复:强制剥夺线程持有的资源、终止死锁的线程、重启进程等(工业级开发中尽量避免,影响程序稳定性)。
3. 活锁(Live Lock):容易被忽视的问题
定义
多个线程为了避免死锁,主动释放资源并重新尝试获取锁,但由于释放和尝试的顺序一致,导致线程始终无法获取到所需的资源,一直处于“释放-尝试”的循环中,这种现象称为活锁。
解决方法
- 线程尝试获取锁失败后,随机休眠一段时间,打破释放和尝试的顺序;
- 为线程设置不同的重试优先级,避免同时尝试。
六、线程池(高并发场景的工程化实现)
1. 线程池的设计初衷
直接创建线程的方式在高并发场景下存在明显的缺陷:
- 线程的创建和销毁有较大的开销,高并发时频繁创建/销毁线程会严重消耗CPU和内存资源;
- 过多的线程会导致CPU上下文切换频繁,降低程序的执行效率;
- 无限制创建线程会导致内存溢出(每个线程有独立的栈空间)。
线程池的核心思想是:提前创建一定数量的线程,放入线程池中,当有任务时,从线程池中取出空闲线程执行任务,任务完成后线程不退出,继续等待下一个任务。
2. 线程池的核心优势
- 降低开销:避免频繁创建/销毁线程,减少系统资源消耗;
- 提高效率:任务到达时无需等待线程创建,直接执行,提升响应速度;
- 资源控制:限制线程池的最大线程数,避免线程过多导致的资源耗尽;
- 易于管理:统一管理线程的创建、销毁、任务分配,便于监控和调优。
3. 线程池的核心组成部分
一个标准的线程池包含以下5个核心部分,缺一不可:
4. 线程池的工作流程
5. 简单线程池的实现(基于pthread)
以下实现一个固定大小的线程池(核心线程数=最大线程数),包含任务队列、工作线程、任务添加、线程池销毁等核心功能,是工业级线程池的基础版本。
核心代码框架
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 任务队列的最大任务数
#define MAX_TASK_NUM 1024
// 线程池的最大线程数
#define MAX_THREAD_NUM 8
// 任务结构体:定义任务的统一接口
typedef struct {
void (*func)(void *arg); // 任务执行函数
void *arg; // 任务参数
} Task;
// 线程池结构体
typedef struct {
Task task_queue[MAX_TASK_NUM]; // 任务队列(数组实现)
int queue_front; // 任务队列头指针
int queue_rear; // 任务队列尾指针
int task_count; // 任务队列中的任务数
pthread_t workers[MAX_THREAD_NUM]; // 工作线程数组
int thread_num; // 实际工作线程数
pthread_mutex_t mutex; // 保护任务队列的互斥锁
pthread_cond_t cond; // 唤醒工作线程的条件变量
int is_running; // 线程池运行状态:1-运行,0-停止
} ThreadPool;
// 全局线程池实例
ThreadPool g_pool;
// 工作线程的执行函数:循环获取任务并执行
void* worker_func(void *arg)
{
ThreadPool *pool = (ThreadPool*)arg;
while (1)
{
pthread_mutex_lock(&pool->mutex);
// 任务队列为空,且线程池运行中,阻塞等待
while (pool->task_count == 0 && pool->is_running)
{
pthread_cond_wait(&pool->cond, &pool->mutex);
}
// 线程池停止,且任务队列为空,退出线程
if (!pool->is_running && pool->task_count == 0)
{
pthread_mutex_unlock(&pool->mutex);
pthread_exit(NULL);
}
// 从任务队列中取出一个任务
Task task = pool->task_queue[pool->queue_front];
pool->queue_front = (pool->queue_front + 1) % MAX_TASK_NUM;
pool->task_count—;
pthread_mutex_unlock(&pool->mutex);
// 执行任务
task.func(task.arg);
free(task.arg); // 释放任务参数的内存
}
pthread_exit(NULL);
}
// 初始化线程池
int thread_pool_init(int thread_num)
{
if (thread_num <= 0 || thread_num > MAX_THREAD_NUM)
{
thread_num = MAX_THREAD_NUM;
}
ThreadPool *pool = &g_pool;
// 初始化任务队列
pool->queue_front = 0;
pool->queue_rear = 0;
pool->task_count = 0;
// 初始化同步互斥资源
pthread_mutex_init(&pool->mutex, NULL);
pthread_cond_init(&pool->cond, NULL);
// 设置线程池状态
pool->is_running = 1;
pool->thread_num = thread_num;
// 创建工作线程
for (int i = 0; i < thread_num; i++)
{
pthread_create(&pool->workers[i], NULL, worker_func, pool);
}
return 0;
}
// 向线程池添加任务
int thread_pool_add_task(void (*func)(void *arg), void *arg)
{
ThreadPool *pool = &g_pool;
pthread_mutex_lock(&pool->mutex);
// 任务队列满,添加失败
if (pool->task_count >= MAX_TASK_NUM)
{
pthread_mutex_unlock(&pool->mutex);
return –1;
}
// 将任务加入任务队列
pool->task_queue[pool->queue_rear].func = func;
pool->task_queue[pool->queue_rear].arg = arg;
pool->queue_rear = (pool->queue_rear + 1) % MAX_TASK_NUM;
pool->task_count++;
// 唤醒一个工作线程
pthread_cond_signal(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
return 0;
}
// 销毁线程池
int thread_pool_destroy()
{
ThreadPool *pool = &g_pool;
pool->is_running = 0;
// 唤醒所有工作线程
pthread_cond_broadcast(&pool->cond);
// 等待所有工作线程退出
for (int i = 0; i < pool->thread_num; i++)
{
pthread_join(pool->workers[i], NULL);
}
// 销毁同步互斥资源
pthread_mutex_destroy(&pool->mutex);
pthread_cond_destroy(&pool->cond);
return 0;
}
// 测试任务:打印任务信息
void test_task(void *arg)
{
int *num = (int*)arg;
printf("工作线程%lu执行任务:%d\\n", (unsigned long)pthread_self(), *num);
}
// 主函数:测试线程池
int main()
{
// 初始化线程池,创建4个工作线程
thread_pool_init(4);
// 向线程池添加10个任务
for (int i = 0; i < 10; i++)
{
int *arg = (int*)malloc(sizeof(int));
*arg = i + 1;
thread_pool_add_task(test_task, arg);
}
// 休眠,保证任务执行完毕
sleep(3);
// 销毁线程池
thread_pool_destroy();
printf("线程池销毁成功!\\n");
return 0;
}
编译与运行
gcc thread_pool.c -o thread_pool -lpthread
./thread_pool
扩展方向(工业级线程池)
- 动态线程池:支持核心线程数和最大线程数,任务过多时创建临时线程,空闲时销毁临时线程;
- 任务优先级:实现带优先级的任务队列,高优先级任务先执行;
- 任务超时:为任务设置超时时间,超时未执行的任务被丢弃;
- 线程池监控:统计任务执行数、线程空闲数、任务等待时间等指标;
- 异常处理:处理任务执行过程中的异常,避免工作线程崩溃。
七、线程开发的工业级规范(结合编程规范)
结合之前的《老夏课堂C++编程规范》,以及工业级线程开发的最佳实践,制定以下线程开发的工业级规范,保证代码的可读性、可维护性和稳定性。
1. 命名规范
- 线程相关变量:小蛇形命名,加线程相关前缀,如thread_id、task_queue、mutex_lock、cond_var;
- 线程执行函数:大驼峰命名,以ThreadFunc结尾,如ProducerThreadFunc、ConsumerThreadFunc;
- 线程池相关函数:大驼峰命名,以ThreadPool为前缀,如ThreadPoolInit、ThreadPoolAddTask;
- 宏定义:全大写+下划线,如MAX_THREAD_NUM、MAX_TASK_NUM、THREAD_STACK_SIZE。
2. 代码格式规范
- 缩进:仅使用4个空格,禁止使用Tab;
- 大括号:左右大括号单独起一行(Allman风格);
- 锁的加解锁:加锁和解锁代码对齐,临界区代码缩进一层,如:pthread_mutex_lock(&mutex);
{
// 临界区代码,缩进一层
g_sum++;
}
pthread_mutex_unlock(&mutex); - 函数拆分:将线程的执行逻辑拆分为多个小函数,每个函数只做一件事,避免超大函数。
3. 头文件与接口规范
- 线程相关的API声明:统一放在单独的头文件中(如thread_pool.h),使用头文件守卫防止多重包含;
- 任务接口:定义统一的任务函数指针类型,如typedef void (*TaskFunc)(void *arg);,提高代码的规范性;
- 前置声明:在头文件中使用前置声明,减少头文件依赖,如typedef struct ThreadPool ThreadPool;。
4. 注释规范(Doxygen)
- 线程池结构体注释:添加@brief和@details,说明结构体的功能和成员含义;
- 线程相关函数注释:添加@brief、@param、@return,说明函数的功能、参数和返回值;
- 锁和条件变量注释:说明锁保护的临界区和条件变量的等待条件;
- 任务函数注释:说明任务的功能和参数含义。
示例:函数注释
/// @brief 初始化线程池
/// @param thread_num 工作线程数,范围1-MAX_THREAD_NUM
/// @return 成功返回0,失败返回-1
int ThreadPoolInit(int thread_num);
网硕互联帮助中心




评论前必须登录!
注册