并发编程-线程
并发编程的原理与应用
一、线程的概念
1.基本定义
进程(Process)是操作系统进行资源分配的基本单位。
线程(Thread)是操作系统能够进行运算调度的最小单位,属于进程的一部分,也被称为轻量级进程(Lightweight Process, LWP)。
一个进程可以包含 多个线程,这些线程共享进程的内存空间(如代码段、数据段、堆等),但拥有独立的 执行栈 和 程序计数器(PC)。
线程是程序执行的最小单元,负责在进程中实际执行代码。
2.核心特性
(1)轻量级
线程的创建、销毁和切换成本远低于进程,因为无需重新分配独立的内存空间,只需分配少量资源(如栈空间、寄存器状态等)。
(2)共享资源
同一进程内的线程共享以下资源:
- 内存地址空间(代码、数据、堆)
- 打开的文件句柄、全局变量、进程级锁等
独立资源包括:
- 线程栈(存储局部变量、函数调用栈帧)
- 程序计数器(记录当前执行的指令位置)
- 寄存器状态(保存当前运算状态)
(3)并发性
多个线程可在 同一 CPU 核心 上通过时间片轮转实现 并发执行,或在 多核 CPU 上实现 并行执行,提升程序效率。
二、线程相关函数
1、线程创建与管理函数
1.创建线程pthread_create()
pthread_create() 是 POSIX 线程库中的一个函数,用于创建新线程。
#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:类型为 const pthread_attr_t,用于设置线程的属性。若传入 NULL,则使用默认属性。
start_routine:类型为 void* ()(void),是新线程开始执行的函数指针,该函数返回值类型为 void,参数类型为 void。
arg:类型为 void*,是传递给 start_routine 函数的参数。如果需要传递多个参数,可以将它们封装在一个结构体中。
返回值:
- 若线程创建成功,返回 0。
- 若创建失败,返回错误码,常见的错误码如下:
EAGAIN:系统资源不足,无法创建新线程。
EINVAL:attr 参数设置的属性无效。
EPERM:没有权限设置 attr 参数指定的属性。
示例代码:
#include <stdio.h>
#include <pthread.h>
// 线程执行的函数
void* thread_function(void* arg) {
int* num = (int*)arg;
printf("线程执行中,参数值为:%d\n", *num);
// 线程返回值
pthread_exit((void*)100);
}
int main() {
pthread_t thread_id;
int param = 42;
void* ret_val;
// 创建新线程
int result = pthread_create(&thread_id, NULL, thread_function, ¶m);
if (result != 0) {
perror("线程创建失败");
return 1;
}
printf("主线程继续执行\n");
// 等待线程结束并获取返回值
result = pthread_join(thread_id, &ret_val);
if (result != 0) {
perror("等待线程失败");
return 1;
}
printf("线程返回值为:%ld\n", (long)ret_val);
return 0;
}
2.线程终止pthread_exit()
pthread_exit() 是 POSIX 线程库中的一个函数,用于终止当前线程的执行。
函数原型
#include <pthread.h>
void pthread_exit(void *retval);
参数:
retval:类型为 void*,是线程的返回值。这个值可以被其他线程通过 pthread_join() 函数获取。
功能:
- pthread_exit() 函数用于显式地终止当前线程的执行。调用该函数后,当前线程会立即终止,并且会释放该线程占用的系统资源(如线程栈、寄存器等)。
- 线程的返回值 retval 会被传递给等待该线程结束的其他线程(通过 pthread_join() 函数)。
- 如果主线程调用 pthread_exit(),它会终止主线程,但不会影响其他已创建的线程,这些线程会继续执行,直到它们自己终止或整个进程被终止。
示例代码:
#include <stdio.h>
#include <pthread.h>
// 线程执行的函数
void* thread_function(void* arg) {
int* num = (int*)arg;
printf("线程开始执行,参数值为:%d\n", *num);
// 模拟一些工作
for (int i = 0; i < 5; i++) {
printf("线程工作中:%d\n", i);
}
// 线程返回值
int result = 100 + *num;
pthread_exit((void*)(long)result); // 将整数转换为 void* 类型
}
int main() {
pthread_t thread_id;
int param = 42;
void* ret_val;
// 创建新线程
int result = pthread_create(&thread_id, NULL, thread_function, ¶m);
if (result != 0) {
perror("线程创建失败");
return 1;
}
printf("主线程继续执行,等待子线程结束...\n");
// 等待线程结束并获取返回值
result = pthread_join(thread_id, &ret_val);
if (result != 0) {
perror("等待线程失败");
return 1;
}
printf("子线程返回值为:%ld\n", (long)ret_val);
return 0;
}
注意事项
- 返回值的传递:由于 retval 是 void* 类型,传递整数时需要进行适当的类型转换(如示例中的 (void*)(long)result)。在接收端也需要进行相应的类型转换。
- 资源释放:调用 pthread_exit() 后,线程的资源会被释放,但如果线程分配了其他资源(如动态内存、文件描述符等),需要在调用 pthread_exit() 之前手动释放这些资源,否则会导致内存泄漏。
- 主线程的终止:如果主线程调用 exit() 或者return语句,整个进程会终止,所有其他线程也会被终止。因此,如果需要让其他线程继续执行,主线程应该调用 pthread_exit() 而不是直接退出。
在线程函数中,return 和 pthread_exit() 都可以终止线程,但有以下区别:
- return:如果在线程函数中使用 return,线程会立即终止,并且返回值会被传递给等待该线程的其他线程。
- pthread_exit():可以在任何地方调用,包括线程函数中调用的其他函数内部,用于终止当前线程。
3.线程的接合 pthread_join()
pthread_join() 是 POSIX 线程库中的一个函数,用于等待指定线程结束来回收其资源并可获得线程退出时返回的返回值。
函数原型:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数:
thread:类型为 pthread_t,指定要等待的线程 ID。
retval:类型为 void**,是一个输出参数,用于存储被等待线程的返回值(即该线程调用 pthread_exit() 或从线程函数中 return 的值)。如果不需要获取返回值,可以传入 NULL。
返回值:
若成功,返回 0。
若失败,返回错误码,常见的错误码如下:
EDEADLK:检测到死锁(例如,线程尝试等待自身)。
EINVAL:thread 不是一个可结合的线程,或指定的线程正在被其他线程等待。
ESRCH:thread 不是一个有效的线程 ID。
功能:
阻塞等待:pthread_join() 会阻塞调用线程,直到指定的 thread 线程终止。
资源回收:当被等待的线程终止后,pthread_join() 会回收该线程的资源(如线程栈、寄存器等),防止出现 “僵尸线程”。
获取返回值:通过 retval 参数,调用者可以获取被等待线程的返回值。
示例代码:
#include <stdio.h>
#include <pthread.h>
// 线程执行的函数
void* thread_function(void* arg) {
int* num = (int*)arg;
printf("线程开始执行,参数值为:%d\n", *num);
// 模拟一些工作
for (int i = 0; i < 3; i++) {
printf("线程工作中:%d\n", i);
}
// 线程返回值
int* result = malloc(sizeof(int));
*result = 100 + *num;
pthread_exit(result); // 返回动态分配的内存地址
}
int main() {
pthread_t thread_id;
int param = 42;
void* ret_val;
// 创建新线程
int result = pthread_create(&thread_id, NULL, thread_function, ¶m);
if (result != 0) {
perror("线程创建失败");
return 1;
}
printf("主线程继续执行,等待子线程结束...\n");
// 等待线程结束并获取返回值
result = pthread_join(thread_id, &ret_val);
if (result != 0) {
perror("等待线程失败");
return 1;
}
int* result_ptr = (int*)ret_val;
printf("子线程返回值为:%d\n", *result_ptr);
free(result_ptr); // 释放动态分配的内存
return 0;
}
4.线程分离pthread_detach()
pthread_detach() 是 POSIX 线程库中的一个函数,用于将指定线程设置为分离状态。处于分离状态的线程在终止时会自动释放其占用的系统资源(如线程栈、寄存器等),无需其他线程调用 pthread_join() 来回收资源。
函数原型
#include <pthread.h>
int pthread_detach(pthread_t thread);
参数
thread:类型为 pthread_t,指定要设置为分离状态的线程 ID。
返回值
若成功,返回 0。
若失败,返回错误码,常见的错误码如下:
ESRCH:thread 不是一个有效的线程 ID。
EINVAL:thread 是一个已经终止的线程,或无法将其设置为分离状态(例如,该线程已经被其他线程等待)。
功能
自动资源回收:分离状态的线程在终止后,系统会自动回收其资源,无需其他线程调用 pthread_join()。
不可等待:一旦线程被设置为分离状态,就不能再使用 pthread_join() 等待它,否则会返回 EINVAL 错误。
返回值不可获取:分离状态的线程终止后,其返回值会被丢弃,无法通过 pthread_join() 获取。
示例代码:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 线程执行的函数
void* thread_function(void* arg) {
int* num = (int*)arg;
printf("线程开始执行,参数值为:%d\n", *num);
// 模拟一些工作
for (int i = 0; i < 5; i++) {
printf("线程工作中:%d\n", i);
sleep(1); // 休眠1秒,模拟耗时操作
}
printf("线程执行完毕\n");
pthread_exit(NULL); // 线程返回NULL
}
int main() {
pthread_t thread_id;
int param = 42;
// 创建新线程
int result = pthread_create(&thread_id, NULL, thread_function, ¶m);
if (result != 0) {
perror("线程创建失败");
return 1;
}
// 将线程设置为分离状态
result = pthread_detach(thread_id);
if (result != 0) {
perror("设置线程分离状态失败");
return 1;
}
printf("主线程继续执行,已将子线程设置为分离状态\n");
// 主线程继续执行其他任务
for (int i = 0; i < 3; i++) {
printf("主线程工作中:%d\n", i);
sleep(1);
}
printf("主线程即将退出,但子线程会继续执行直到完成\n");
return 0;
}
注意事项
何时分离:可以在创建线程后立即调用 pthread_detach(),也可以在线程内部调用 pthread_detach(pthread_self()) 来将自身设置为分离状态。
替代方法:除了使用 pthread_detach(),还可以在创建线程时通过设置线程属性来创建分离线程:
pthread_t thread_id;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&thread_id, &attr, thread_function, NULL);
pthread_attr_destroy(&attr);
资源管理:分离状态的线程无法通过 pthread_join() 获取返回值,因此如果线程分配了动态资源(如 malloc() 分配的内存),需要在线程内部自行释放,否则会导致内存泄漏。
适用场景:pthread_detach() 适用于以下场景:
- 不需要获取线程的返回值。
- 希望线程在终止后自动释放资源,避免编写 pthread_join() 代码。
- 长时间运行的后台线程(如守护线程)。
与 pthread_join () 的对比
| 特性 | pthread_join() | pthread_detach() |
|---|---|---|
| 资源回收 | 需要调用 pthread_join() 回收 | 线程终止后自动回收 |
| 返回值 | 可以获取线程返回值 | 无法获取返回值,返回值被丢弃 |
| 线程状态 | 线程是可结合的(默认) | 线程是分离的 |
| 阻塞调用 | 是(直到线程终止) | 否(立即返回) |
| 适用场景 | 需要返回值、需要等待线程结束 | 不需要返回值、希望自动回收资源 |
2、线程属性操作
1.线程属性的初始化与销毁
线程属性对象用于设置线程的各种属性(如分离状态、栈大小等)。使用线程属性前需要进行初始化,使用完毕后需要销毁以释放资源。
线程属性初始化:pthread_attr_init()
函数原型
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
参数说明
attr:类型为 pthread_attr_t*,指向要初始化的线程属性对象。
返回值
若成功,返回 0。
若失败,返回错误码(如 ENOMEM,表示内存不足)。
功能说明
- 初始化一个线程属性对象,将其设置为默认值。
- 初始化后的属性对象可用于 pthread_create() 函数,或通过其他属性设置函数(如 pthread_attr_setdetachstate())修改特定属性。
线程属性销毁:pthread_attr_destroy()
函数原型
#include <pthread.h>
int pthread_attr_destroy(pthread_attr_t *attr);
参数说明
attr:类型为 pthread_attr_t*,指向要销毁的线程属性对象。
返回值
若成功,返回 0。
若失败,返回错误码(如 EINVAL,表示 attr 无效)。
功能说明
- 销毁线程属性对象,释放其占用的资源。
- 销毁后,attr 对象不再有效,除非再次调用 pthread_attr_init() 重新初始化。
示例代码:
#include <stdio.h>
#include <pthread.h>
// 线程执行的函数
void* thread_function(void* arg) {
printf("分离线程开始执行\n");
// 线程工作...
pthread_exit(NULL);
}
int main() {
pthread_t thread_id;
pthread_attr_t attr;
int result;
// 初始化线程属性对象
result = pthread_attr_init(&attr);
if (result != 0) {
perror("初始化线程属性失败");
return 1;
}
// 设置线程属性为分离状态
result = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (result != 0) {
perror("设置分离状态失败");
pthread_attr_destroy(&attr); // 销毁已初始化的属性对象
return 1;
}
// 使用设置好的属性创建线程
result = pthread_create(&thread_id, &attr, thread_function, NULL);
if (result != 0) {
perror("创建线程失败");
pthread_attr_destroy(&attr); // 销毁已初始化的属性对象
return 1;
}
// 线程属性对象已被 pthread_create() 使用,可安全销毁
result = pthread_attr_destroy(&attr);
if (result != 0) {
perror("销毁线程属性失败");
// 继续执行,不影响程序正确性
}
printf("主线程继续执行,分离线程会自动释放资源\n");
// 主线程继续工作...
return 0;
}
注意事项:
- 属性对象的生命周期:
线程属性对象在调用 pthread_attr_init() 后创建,在调用 pthread_attr_destroy() 后销毁。
销毁属性对象不会影响已基于该属性创建的线程。 - 资源释放:
pthread_attr_destroy() 仅释放属性对象本身占用的资源,不会释放通过属性设置的其他资源(如通过 pthread_attr_setstack() 设置的栈内存)。 - 重复初始化与销毁:
已销毁的属性对象必须重新调用 pthread_attr_init() 才能再次使用。
对已初始化的属性对象重复调用 pthread_attr_init() 会导致未定义行为,应避免。 - 错误处理:
实际编程中,应检查 pthread_attr_init() 和 pthread_attr_destroy() 的返回值,确保操作成功。
若 pthread_attr_init() 失败,不应使用该属性对象;若 pthread_attr_destroy() 失败,可能需要手动处理资源泄漏。
常见线程属性设置函数
| 函数 | 功能描述 |
|---|---|
| pthread_attr_setdetachstate | 设置线程分离状态 |
| pthread_attr_setstacksize | 设置线程栈大小 |
| pthread_attr_setguardsize | 设置线程栈警戒区大小 |
| pthread_attr_setschedpolicy | 设置线程调度策略 |
| pthread_attr_setschedparam | 设置线程调度参数(如优先级) |
2.设置 / 获取线程分离状态
函数原型:
#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 设置分离状态(PTHREAD_CREATE_DETACHED 或 PTHREAD_CREATE_JOINABLE)
int pthread_attr_getdetachstate(pthread_attr_t *attr,
int *detachstate);
// 获取分离状态
1.线程分离状态概述
分离状态:线程可以被创建为可结合的(joinable)或分离的(detached)
可结合状态:线程结束后,需要通过pthread_join()回收资源
分离状态:线程结束后,系统自动回收资源,无法对其调用pthread_join()
2.设置线程分离状态的方法
有两种主要方式可以设置线程为分离状态:
方法一:在线程创建后使用pthread_detach()分离
#include <pthread.h>
void* thread_function(void* arg) {
// 线程执行体
return NULL;
}
int main() {
pthread_t thread;
// 创建线程
pthread_create(&thread, NULL, thread_function, NULL);
// 设置线程为分离状态
pthread_detach(thread);
// 主线程继续执行,无需等待该线程结束
return 0;
}
方法二:在线程创建前通过属性设置其分离属性pthread_attr_setdetachstate()
#include <pthread.h>
void* thread_function(void* arg) {
// 线程执行体
return NULL;
}
int main() {
pthread_t thread;
pthread_attr_t attr;
// 初始化线程属性对象
pthread_attr_init(&attr);
// 设置线程分离状态
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 创建线程时应用属性
pthread_create(&thread, &attr, thread_function, NULL);
// 销毁属性对象
pthread_attr_destroy(&attr);
// 主线程继续执行
return 0;
}
3.获取线程分离状态
获取线程的分离状态需要通过pthread_attr_getdetachstate()获取线程属性对象的分离属性:
#include <pthread.h>
#include <stdio.h>
int main() {
pthread_attr_t attr;
int detachstate;
// 初始化属性对象
pthread_attr_init(&attr);
// 获取当前分离状态
pthread_attr_getdetachstate(&attr, &detachstate);
if (detachstate == PTHREAD_CREATE_DETACHED) {
printf("线程属性:分离状态\n");
} else {
printf("线程属性:可结合状态\n");
}
// 销毁属性对象
pthread_attr_destroy(&attr);
return 0;
}
4.完整示例:创建分离线程
创建一个分离线程并获取其状态:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void* thread_function(void* arg) {
printf("分离线程开始执行,ID: %lu\n", pthread_self());
sleep(2);
printf("分离线程执行完毕\n");
pthread_exit(NULL);
}
int main() {
pthread_t thread;
pthread_attr_t attr;
int detachstate;
// 初始化属性对象
pthread_attr_init(&attr);
// 获取初始分离状态
pthread_attr_getdetachstate(&attr, &detachstate);
printf("初始分离状态: %s\n",
detachstate == PTHREAD_CREATE_DETACHED ? "分离" : "可结合");
// 设置为分离状态
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 创建分离线程
if (pthread_create(&thread, &attr, thread_function, NULL) != 0) {
perror("线程创建失败");
return 1;
}
// 验证分离状态
pthread_attr_getdetachstate(&attr, &detachstate);
printf("设置后的分离状态: %s\n",
detachstate == PTHREAD_CREATE_DETACHED ? "分离" : "可结合");
// 销毁属性对象
pthread_attr_destroy(&attr);
// 主线程继续执行,不等待分离线程
printf("主线程继续执行...\n");
sleep(1);
printf("主线程即将退出...\n");
// 注意:分离线程会在后台继续执行,直到完成
return 0;
}
3.设置 / 获取线程栈大小
#include <pthread.h>
int pthread_attr_setstacksize(
pthread_attr_t *attr, size_t stacksize); // 设置栈大小(字节)
int pthread_attr_getstacksize(
pthread_attr_t *attr, size_t *stacksize); // 获取栈大小
3、线程同步与互斥机制
1.互斥锁(Mutex)
#include <pthread.h>
pthread_mutex_t mutex; // 声明互斥锁
// 初始化互斥锁
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_trylock(pthread_mutex_t *mutex);
// 销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
使用 C 语言进行多线程编程时,互斥锁(Mutex)是最常用的同步机制之一,用于保护共享资源,防止多个线程同时访问而导致的数据竞争问题。
1.互斥锁基本概念
作用:保证同一时间只有一个线程可以访问共享资源。
状态:互斥锁有两种状态:锁定(locked)和解锁(unlocked)。
操作:
pthread_mutex_lock():加锁(如果锁已被其他线程持有,则当前线程阻塞等待)。
pthread_mutex_unlock():解锁(释放锁,唤醒等待该锁的线程)。
pthread_mutex_trylock():尝试加锁(立即返回,不会阻塞)。
pthread_mutex_timedlock():超时锁(设置超时时间,避免线程长时间阻塞)。
2.互斥锁的初始化与销毁
互斥锁使用前需要初始化,使用完毕后需要销毁。有两种初始化方式:
静态初始化(适用于全局或静态互斥锁)
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化(适用于局部互斥锁)
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); // NULL表示使用默认属性
// 使用完毕后销毁
pthread_mutex_destroy(&mutex);
3.互斥锁的基本用法
以下是一个保护共享变量的示例:
#include <pthread.h>
#include <stdio.h>
int shared_counter = 0; // 共享资源
pthread_mutex_t counter_mutex; // 互斥锁
void* increment(void* arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&counter_mutex); // 加锁
shared_counter++; // 临界区
pthread_mutex_unlock(&counter_mutex); // 解锁
}
return NULL;
}
int main() {
pthread_t threads[2];
// 初始化互斥锁
pthread_mutex_init(&counter_mutex, NULL);
// 创建两个线程
pthread_create(&threads[0], NULL, increment, NULL);
pthread_create(&threads[1], NULL, increment, NULL);
// 等待线程结束
pthread_join(threads[0], NULL);
pthread_join(threads[1], NULL);
// 销毁互斥锁
pthread_mutex_destroy(&counter_mutex);
printf("共享计数器的值: %d\n", shared_counter); // 输出应为200000
return 0;
}
4.带超时的锁操作
使用 pthread_mutex_timedlock() 可以设置超时时间,避免线程长时间阻塞:
#include <pthread.h>
#include <time.h>
#include <stdio.h>
pthread_mutex_t mutex;
void* thread_function(void* arg) {
struct timespec timeout;
// 设置超时时间为当前时间 + 2秒
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += 2;
int ret = pthread_mutex_timedlock(&mutex, &timeout);
if (ret == 0) {
printf("成功获取锁\n");
// 临界区代码
pthread_mutex_unlock(&mutex);
} else if (ret == ETIMEDOUT) {
printf("获取锁超时\n");
} else {
printf("获取锁失败,错误码: %d\n", ret);
}
return NULL;
}
5.互斥锁属性
通过 pthread_mutexattr_t 可以设置互斥锁的属性,例如类型(普通锁、递归锁、检错锁等):
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
// 初始化属性对象
pthread_mutexattr_init(&attr);
// 设置为递归锁(同一线程可多次加锁)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 使用属性初始化互斥锁
pthread_mutex_init(&mutex, &attr);
// 销毁属性对象
pthread_mutexattr_destroy(&attr);
6.注意事项
避免死锁:
确保线程以相同顺序获取多个锁。
使用 pthread_mutex_trylock() 避免循环等待。
锁粒度:
不要过度使用锁(锁粒度太大影响性能)。
也不要使用太少(锁粒度太小可能无法保证线程安全)。
异常处理:
在临界区内发生异常(如 return、break)时,确保释放锁。
2.条件变量(Condition Variable)
#include <pthread.h>
pthread_cond_t cond; // 声明条件变量
// 初始化条件变量
int pthread_cond_init(
pthread_cond_t *cond,
const pthread_condattr_t *attr
);
// 等待条件变量
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);
条件变量(Condition Variable)是一种用于线程间同步的机制,通常与互斥锁配合使用。条件变量允许线程在某个条件不满足时等待,直到其他线程通知该条件已满足。
1.条件变量基本概念
作用:实现线程间的等待 - 通知机制,避免线程不断轮询检查某个条件。
核心操作:
pthread_cond_wait():释放互斥锁并阻塞线程,直到收到通知后重新加锁。
pthread_cond_signal():唤醒一个等待该条件的线程。
pthread_cond_broadcast():唤醒所有等待该条件的线程。
与互斥锁的关系:条件变量必须与互斥锁配合使用,以确保条件检查的原子性。
2.条件变量的初始化与销毁
条件变量使用前需要初始化,使用完毕后需要销毁。有两种初始化方式:
静态初始化(适用于全局或静态条件变量)
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态初始化(适用于局部条件变量)
c
pthread_cond_t cond;
pthread_cond_init(&cond, NULL); // NULL表示使用默认属性
// 使用完毕后销毁
pthread_cond_destroy(&cond);
3.条件变量的基本用法
以下是一个生产者 - 消费者模型的示例,展示条件变量的典型用法:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0; // 缓冲区中的元素数量
int in = 0; // 写入位置
int out = 0; // 读取位置
pthread_mutex_t mutex; // 互斥锁
pthread_cond_t not_full; // 缓冲区非满条件
pthread_cond_t not_empty;// 缓冲区非空条件
// 生产者线程函数
void* producer(void* arg) {
for (int i = 0; i < 10; i++) {
// 加锁
pthread_mutex_lock(&mutex);
// 等待缓冲区非满
while (count == BUFFER_SIZE) {
pthread_cond_wait(¬_full, &mutex);
}
// 生产数据
buffer[in] = i;
printf("生产者生产: %d (位置: %d)\n", buffer[in], in);
in = (in + 1) % BUFFER_SIZE;
count++;
// 通知缓冲区非空
pthread_cond_signal(¬_empty);
// 解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
// 消费者线程函数
void* consumer(void* arg) {
for (int i = 0; i < 10; i++) {
// 加锁
pthread_mutex_lock(&mutex);
// 等待缓冲区非空
while (count == 0) {
pthread_cond_wait(¬_empty, &mutex);
}
// 消费数据
int data = buffer[out];
printf("消费者消费: %d (位置: %d)\n", data, out);
out = (out + 1) % BUFFER_SIZE;
count--;
// 通知缓冲区非满
pthread_cond_signal(¬_full);
// 解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t producer_thread, consumer_thread;
// 初始化互斥锁和条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(¬_full, NULL);
pthread_cond_init(¬_empty, NULL);
// 创建生产者和消费者线程
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&consumer_thread, NULL, consumer, NULL);
// 等待线程结束
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
return 0;
}
4.带超时的等待
使用 pthread_cond_timedwait() 可以设置等待超时时间,避免线程永久阻塞:
#include <pthread.h>
#include <time.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
void* thread_function(void* arg) {
struct timespec timeout;
// 设置超时时间为当前时间 + 2秒
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += 2;
pthread_mutex_lock(&mutex);
// 带超时的等待
int ret = pthread_cond_timedwait(&cond, &mutex, &timeout);
if (ret == 0) {
printf("收到通知,条件满足\n");
} else if (ret == ETIMEDOUT) {
printf("等待超时\n");
}
pthread_mutex_unlock(&mutex);
return NULL;
}
5.广播通知
使用 pthread_cond_broadcast() 可以唤醒所有等待该条件的线程:
// 主线程中
pthread_mutex_lock(&mutex);
// 设置条件
pthread_cond_broadcast(&cond); // 唤醒所有等待的线程
pthread_mutex_unlock(&mutex);
6.注意事项
- while 循环检查条件:
必须使用 while 循环而非 if,因为可能存在虚假唤醒(spurious wakeup)。 - 原子性要求:
在调用 pthread_cond_wait() 前必须先加锁,且该锁必须与条件变量关联。 - 通知时机:
通常在修改条件变量对应的共享状态后才发送通知。 - 性能考虑:
pthread_cond_broadcast() 会唤醒所有线程,可能导致性能问题,尽量使用 pthread_cond_signal()。
3.读写锁(Read-Write Lock)
#include <pthread.h>
pthread_rwlock_t rwlock; // 声明读写锁
// 初始化与销毁
int pthread_rwlock_init(
pthread_rwlock_t *rwlock,
const pthread_rwlockattr_t *attr
);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 读锁操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 写锁操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
读写锁(Read-Write Lock)是一种特殊的锁机制,用于优化对共享资源的访问。与普通互斥锁不同,读写锁允许多个线程同时读取共享资源,但在写入时需要独占访问。这种特性使得读写锁特别适合于读多写少的场景。
1.读写锁的基本概念
特性:
读锁(共享锁):允许多个线程同时获取,用于并发读取共享资源。
写锁(排他锁):同一时间只能被一个线程获取,且获取时不能有其他读锁或写锁。
适用场景:读操作频繁、写操作较少的场景(如配置文件读取、缓存更新)。
优势:相比普通互斥锁,读写锁能显著提高读并发性能。
2.读写锁的初始化与销毁
读写锁使用前需要初始化,使用完毕后需要销毁。有两种初始化方式:
静态初始化(适用于全局或静态读写锁)
#include <pthread.h>
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
动态初始化(适用于局部读写锁)
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL); // NULL表示使用默认属性
// 使用完毕后销毁
pthread_rwlock_destroy(&rwlock);
3.读写锁的基本操作
获取读锁
pthread_rwlock_rdlock(&rwlock); // 阻塞式获取读锁
// 或使用非阻塞版本
int ret = pthread_rwlock_tryrdlock(&rwlock);
获取写锁
pthread_rwlock_wrlock(&rwlock); // 阻塞式获取写锁
// 或使用非阻塞版本
int ret = pthread_rwlock_trywrlock(&rwlock);
释放锁
pthread_rwlock_unlock(&rwlock); // 释放读锁或写锁
4.读写锁的示例:读多写少场景
以下是一个使用读写锁保护共享数据的示例,模拟配置文件的读取和更新:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 共享资源:配置数据
int config_data = 0;
pthread_rwlock_t rwlock;
// 读取线程函数
void* reader(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 5; i++) {
// 获取读锁
pthread_rwlock_rdlock(&rwlock);
printf("读者 %d 读取配置: %d\n", id, config_data);
usleep(100000); // 模拟读取耗时
// 释放读锁
pthread_rwlock_unlock(&rwlock);
usleep(200000); // 模拟处理耗时
}
free(arg);
return NULL;
}
// 写入线程函数
void* writer(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 3; i++) {
// 获取写锁
pthread_rwlock_wrlock(&rwlock);
config_data++; // 更新配置
printf("写者 %d 更新配置为: %d\n", id, config_data);
usleep(200000); // 模拟写入耗时
// 释放写锁
pthread_rwlock_unlock(&rwlock);
usleep(500000); // 模拟处理耗时
}
free(arg);
return NULL;
}
int main() {
pthread_t readers[5], writers[2];
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 创建多个读线程和写线程
for (int i = 0; i < 5; i++) {
int* id = malloc(sizeof(int));
*id = i;
pthread_create(&readers[i], NULL, reader, id);
}
for (int i = 0; i < 2; i++) {
int* id = malloc(sizeof(int));
*id = i;
pthread_create(&writers[i], NULL, writer, id);
}
// 等待所有线程结束
for (int i = 0; i < 5; i++) {
pthread_join(readers[i], NULL);
}
for (int i = 0; i < 2; i++) {
pthread_join(writers[i], NULL);
}
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
5.读写锁属性
通过 pthread_rwlockattr_t 可以设置读写锁的属性,例如优先级策略:
pthread_rwlock_t rwlock;
pthread_rwlockattr_t attr;
// 初始化属性对象
pthread_rwlockattr_init(&attr);
// 设置写锁优先级更高(Linux默认行为)
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
// 使用属性初始化读写锁
pthread_rwlock_init(&rwlock, &attr);
// 销毁属性对象
pthread_rwlockattr_destroy(&attr);
6.注意事项
- 避免写锁饥饿:
如果读操作过于频繁,可能导致写线程长时间无法获取锁。某些系统提供了偏向写锁的选项(如上述示例)。 - 锁的降级与升级:
锁降级:持有写锁的线程可以释放写锁后获取读锁,但需注意原子性。
锁升级:持有读锁的线程不能直接升级为写锁,需先释放读锁,否则可能导致死锁。 - 性能考量:
读写锁的开销通常比普通互斥锁大,因此仅在真正需要并发读取的场景中使用。
4.POSIX信号量
1.匿名信号量
POSIX 信号量(Semaphore)是一种用于线程间同步的机制,与互斥锁和条件变量类似,但功能更强大。信号量可以看作是一个计数器,用于控制对共享资源的访问数量。信号量分为匿名信号量(主要是在一个进程之间的多个线程,只存在与内存中)和具名信号量(主要用在多个进程之间)。
1.信号量的基本概念
计数器:信号量维护一个整数值,表示可用资源的数量。
核心操作:
wait()(或P()):将信号量值减 1。若值为 0,则线程阻塞等待。
post()(或V()):将信号量值加 1,唤醒可能正在等待的线程。
类型:
二进制信号量:值只能为 0 或 1,类似互斥锁。
计数信号量:值可以为任意非负整数,用于限制对一组资源的访问。
2.POSIX 信号量的 API
POSIX 信号量分为进程内信号量(线程间共享)和进程间信号量(通过文件系统路径名标识)。
头文件与初始化
#include <semaphore.h>
sem_t semaphore; // 定义信号量
// 初始化信号量(pshared=0表示线程间共享)
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 销毁信号量
int sem_destroy(sem_t *sem);
信号量操作
// 等待(P操作):信号量值减1,若值为0则阻塞
int sem_wait(sem_t *sem);
// 尝试等待(非阻塞)
int sem_trywait(sem_t *sem);
// 带超时的等待
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
// 发布(V操作):信号量值加1,唤醒等待线程
int sem_post(sem_t *sem);
// 获取当前信号量值(调试用)
int sem_getvalue(sem_t *sem, int *sval);
3.信号量的典型应用场景
示例1:二进制信号量(替代互斥锁)
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
sem_t mutex; // 二进制信号量
int shared_counter = 0;
void* thread_function(void* arg) {
for (int i = 0; i < 100000; i++) {
sem_wait(&mutex); // 加锁
shared_counter++; // 临界区
sem_post(&mutex); // 解锁
}
return NULL;
}
int main() {
pthread_t threads[2];
// 初始化信号量(值为1,表示资源可用)
sem_init(&mutex, 0, 1);
// 创建线程
pthread_create(&threads[0], NULL, thread_function, NULL);
pthread_create(&threads[1], NULL, thread_function, NULL);
// 等待线程结束
pthread_join(threads[0], NULL);
pthread_join(threads[1], NULL);
// 销毁信号量
sem_destroy(&mutex);
printf("共享计数器的值: %d\n", shared_counter); // 输出应为200000
return 0;
}
示例 2:生产者 - 消费者模型(使用计数信号量)
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
sem_t empty; // 空闲槽位计数
sem_t full; // 已使用槽位计数
pthread_mutex_t mutex; // 保护缓冲区访问
// 生产者线程函数
void* producer(void* arg) {
for (int i = 0; i < 10; i++) {
// 等待空闲槽位
sem_wait(&empty);
// 进入临界区
pthread_mutex_lock(&mutex);
buffer[in] = i;
printf("生产者生产: %d (位置: %d)\n", buffer[in], in);
in = (in + 1) % BUFFER_SIZE;
// 离开临界区
pthread_mutex_unlock(&mutex);
// 通知已有数据
sem_post(&full);
}
return NULL;
}
// 消费者线程函数
void* consumer(void* arg) {
for (int i = 0; i < 10; i++) {
// 等待已有数据
sem_wait(&full);
// 进入临界区
pthread_mutex_lock(&mutex);
int data = buffer[out];
printf("消费者消费: %d (位置: %d)\n", data, out);
out = (out + 1) % BUFFER_SIZE;
// 离开临界区
pthread_mutex_unlock(&mutex);
// 通知有空槽位
sem_post(&empty);
}
return NULL;
}
int main() {
pthread_t producer_thread, consumer_thread;
// 初始化信号量和互斥锁
sem_init(&empty, 0, BUFFER_SIZE); // 初始时有BUFFER_SIZE个空闲槽位
sem_init(&full, 0, 0); // 初始时没有已使用槽位
pthread_mutex_init(&mutex, NULL);
// 创建线程
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&consumer_thread, NULL, consumer, NULL);
// 等待线程结束
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
// 销毁资源
sem_destroy(&empty);
sem_destroy(&full);
pthread_mutex_destroy(&mutex);
return 0;
}
4.注意事项
- 信号量与互斥锁的区别:
信号量可以有多个资源实例(值 > 1),而互斥锁只能被一个线程持有。
信号量的 post() 操作可以由任意线程执行,而互斥锁的解锁必须由加锁线程执行。 - 避免死锁:
与互斥锁相同,使用信号量时也需避免循环等待。 - 性能考量:
信号量的开销通常比互斥锁略大,因此在仅需互斥的场景下优先使用互斥锁。
2.具名信号量
具名信号量(Named Semaphores) 是 POSIX 信号量的一种特殊类型,与普通信号量(基于内存的信号量)不同,具名信号量通过文件系统路径名标识,可用于不同进程间的同步。
1.具名信号量的基本概念
命名机制:通过以 / 开头的路径名(如 /my_semaphore)标识,存储在虚拟文件系统(通常是 tmpfs)中。
进程间共享:不同进程可以通过相同的名称打开同一个信号量,实现跨进程同步。
持久性:具名信号量的生命周期独立于进程,创建后需显式删除。
2.具名信号量的 API
头文件与创建 / 打开
#include <fcntl.h> // O_CREAT, O_EXCL等标志
#include <sys/stat.h> // 权限定义
#include <semaphore.h>
// 创建或打开具名信号量
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
// 关闭具名信号量(减少引用计数)
int sem_close(sem_t *sem);
// 删除具名信号量(当引用计数为0时销毁)
int sem_unlink(const char *name);
信号量操作(与普通信号量相同)
// 等待(P操作):信号量值减1,若值为0则阻塞
int sem_wait(sem_t *sem);
// 尝试等待(非阻塞)
int sem_trywait(sem_t *sem);
// 带超时的等待
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
// 发布(V操作):信号量值加1,唤醒等待线程
int sem_post(sem_t *sem);
3.具名信号量的典型应用场景
示例 1:进程间共享资源的互斥访问
以下示例展示了两个独立进程如何使用具名信号量保护共享内存:
进程 A(创建信号量并访问共享资源)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <semaphore.h>
#include <unistd.h>
#define SHM_SIZE 1024
#define SEM_NAME "/my_semaphore"
int main() {
// 创建共享内存
int fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);
ftruncate(fd, SHM_SIZE);
char *data = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 创建具名信号量(初始值为1,表示资源可用)
sem_t *sem = sem_open(SEM_NAME, O_CREAT | O_EXCL, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open failed");
exit(1);
}
// 模拟多次访问共享资源
for (int i = 0; i < 3; i++) {
sem_wait(sem); // 加锁
sprintf(data, "Message from Process A: %d", i);
printf("进程A写入: %s\n", data);
sem_post(sem); // 解锁
sleep(1);
}
// 关闭信号量但不删除
sem_close(sem);
// 解除映射并关闭共享内存
munmap(data, SHM_SIZE);
close(fd);
// 提示用户手动删除信号量(或在进程B完成后删除)
printf("按Enter键删除信号量和共享内存...\n");
getchar();
sem_unlink(SEM_NAME);
shm_unlink("/my_shared_memory");
return 0;
}
进程 B(打开已存在的信号量并访问共享资源)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <semaphore.h>
#define SHM_SIZE 1024
#define SEM_NAME "/my_semaphore"
int main() {
// 打开共享内存
int fd = shm_open("/my_shared_memory", O_RDWR, 0666);
char *data = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 打开已存在的具名信号量
sem_t *sem = sem_open(SEM_NAME, 0);
if (sem == SEM_FAILED) {
perror("sem_open failed");
exit(1);
}
// 模拟多次访问共享资源
for (int i = 0; i < 3; i++) {
sem_wait(sem); // 加锁
printf("进程B读取: %s\n", data);
sem_post(sem); // 解锁
sleep(1);
}
// 关闭信号量
sem_close(sem);
// 解除映射并关闭共享内存
munmap(data, SHM_SIZE);
close(fd);
return 0;
}
4.编译与运行
编译时需要链接 pthread 和 rt 库:
gcc -o process_a process_a.c -pthread -lrt
gcc -o process_b process_b.c -pthread -lrt
运行时,先启动进程 A,再启动进程 B:
./process_a & # 后台运行进程A
./process_b # 前台运行进程B
5.注意事项
-
信号量命名规则:
名称必须以 / 开头,且后续不能包含其他斜杠(如 /my_sem 合法,/dir/sem 非法)。
不同系统对名称长度和字符集有不同限制,建议使用简单名称。 -
资源管理:
sem_close() 仅关闭当前进程的信号量句柄,不会销毁信号量。
sem_unlink() 标记信号量为待删除,当所有进程都关闭该信号量后才会真正销毁。 -
错误处理:
创建信号量时使用 O_EXCL 标志可避免意外重用已存在的信号量。
确保在程序退出前正确关闭和删除信号量,避免资源泄漏。
4、示例代码:线程创建与同步
1.基本线程创建
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void* thread_function(void* arg) {
int id = *(int*)arg;
printf("线程 %d 开始执行\n", id);
sleep(2);
printf("线程 %d 执行结束\n", id);
pthread_exit((void*)(long)id); // 返回线程ID
}
int main() {
pthread_t threads[3];
int thread_ids[3] = {1, 2, 3};
void* result;
// 创建3个线程
for (int i = 0; i < 3; i++) {
if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
perror("线程创建失败");
return 1;
}
}
// 等待所有线程结束
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], &result);
printf("线程 %ld 已结束\n", (long)result);
}
return 0;
}
2.使用互斥锁保护共享资源
#include <pthread.h>
#include <stdio.h>
int shared_counter = 0;
pthread_mutex_t mutex;
void* increment(void* arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mutex);
shared_counter++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
printf("共享计数器最终值: %d\n", shared_counter); // 应输出200000
return 0;
}
3.使用条件变量实现生产者 - 消费者模型
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0;
int in = 0;
int out = 0;
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;
void* producer(void* arg) {
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex);
// 等待缓冲区不为满
while (count == BUFFER_SIZE) {
pthread_cond_wait(¬_full, &mutex);
}
buffer[in] = i;
in = (in + 1) % BUFFER_SIZE;
count++;
printf("生产者生产: %d (缓冲区数量: %d)\n", i, count);
// 通知消费者缓冲区非空
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex);
// 等待缓冲区不为空
while (count == 0) {
pthread_cond_wait(¬_empty, &mutex);
}
int item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
printf("消费者消费: %d (缓冲区数量: %d)\n", item, count);
// 通知生产者缓冲区非满
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t producer_thread, consumer_thread;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(¬_full, NULL);
pthread_cond_init(¬_empty, NULL);
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&consumer_thread, NULL, consumer, NULL);
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
return 0;
}
5、死锁的产生与解决
在多线程或多进程编程中,死锁(Deadlock) 是一种严重的程序状态,多个线程或进程因互相等待对方释放资源而陷入无限等待。以下是关于死锁的详细分析及解决方案:
死锁的四个必要条件(Coffman 条件)
死锁发生必须同时满足以下四个条件:
- 互斥条件(资源互斥):资源不能被多个线程 / 进程同时使用(如互斥锁)。
- 占有并等待(请求且保持):线程 / 进程至少占有一个资源,并等待其他资源。
- 不可抢占(不可剥夺):资源只能由持有者主动释放,不能被其他线程 / 进程强行抢占。
- 循环等待:多个线程 / 进程形成环形的资源等待链,每个线程 / 进程都在等待下一个线程 / 进程占有的资源。
2.死锁的解决方案
2.1 预防死锁(破坏必要条件)
破坏互斥条件:
理论上可行,但许多资源(如文件、锁)本身具有互斥性,难以实现。
破坏占有并等待:
一次性获取所有资源:线程在开始前请求所有需要的资源,否则不执行。
破坏不可抢占:
使用可抢占锁(如支持超时的锁):
// 使用pthread_mutex_timedlock实现可抢占
struct timespec timeout;
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += 1; // 1秒超时
if (pthread_mutex_timedlock(&mutex, &timeout) == ETIMEDOUT) {
// 获取锁超时,释放已持有的资源并重试
pthread_mutex_unlock(&other_mutex);
retry_later();
}
破坏循环等待:
按顺序获取锁:所有线程必须按相同的顺序获取锁。
// 线程1和线程2都按锁1→锁2的顺序获取
void* thread1(void* arg) {
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);
// 临界区
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void* thread2(void* arg) {
pthread_mutex_lock(&mutex1); // 与线程1保持相同顺序
pthread_mutex_lock(&mutex2);
// 临界区
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
2.2避免死锁(动态检测)
银行家算法(Banker's Algorithm):
在分配资源前检查系统状态是否安全,确保不会进入可能导致死锁的状态。
适用于资源数量固定的场景,实现复杂,实际应用较少。
2.3检测与恢复
- 死锁检测算法:
定期检查资源分配图,发现循环等待时触发恢复。
Linux 提供 pstack、gdb 等工具可用于分析死锁。 - 恢复方法:
终止进程:强制终止一个或多个线程 / 进程。
资源抢占:暂时剥夺某些进程的资源。
2.4忽略死锁(鸵鸟策略)
对于某些发生概率极低的场景,选择忽略死锁,让系统在发生死锁时崩溃重启。
适用于死锁成本低于预防成本的情况(如某些嵌入式系统)。
3.最佳实践
- 减少锁的使用:
使用无锁数据结构(如原子操作、CAS)。
缩小锁的粒度,只在关键代码段加锁。 - 保持锁的获取顺序一致:
为所有锁分配全局顺序,所有线程严格按顺序获取。 - 使用带超时的锁:
pthread_mutex_timedlock() 避免永久等待。 - 设计简单的锁层次:
避免复杂的嵌套锁,减少死锁风险。
6、注意事项
- 编译选项:必须使用 -lpthread 链接 pthread 库,否则会出现链接错误。
- 线程安全:共享资源访问必须通过锁保护,避免竞态条件。
- 内存管理:分离状态的线程结束后自动释放资源,而可连接状态的线程需通过 pthread_join() 回收资源。
- 死锁避免:加锁顺序需一致,避免循环等待。
- 可移植性:pthread 是 POSIX 标准,但不同系统实现可能略有差异。
三、线程调度策略
在Linux系统中,线程是操作系统调度资源的最小单位。而Linux系统的内核是抢占式的,优先级高的任务会抢占优先级低的运行资源。当优先级相同时,任务之间会进行轮转达到任务并发的目的。
Linux系统的任务优先级分为两种:一种是静态优先级。一种是动态优先级。
1、静态优先级
任务一旦设置好优先级就不能改变,相当于任务本身的属性。线程的优先级范围为099,静态优先级越大则任务优先级越高。普通任务的优先级为0,系统任务的优先级为199。意味着普通任务无法跟系统任务争抢资源,普通任务之间用动态优先级竞争资源。
相关函数:
在 Linux 多线程编程中,pthread_attr_setinheritsched() 和 pthread_attr_getinheritsched() 是用于控制线程调度属性继承方式的两个关键函数。它们共同决定了新创建的线程是继承父线程的调度参数,还是使用线程属性对象中显式设置的参数。
1.函数原型
#include <pthread.h>
// 设置线程调度属性的继承方式
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
// 获取线程调度属性的继承方式
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched);
关键参数:
attr:线程属性对象指针,通过 pthread_attr_init() 初始化。
inheritsched:继承方式,取值为:
PTHREAD_INHERIT_SCHED(默认值):继承父线程的调度策略。
PTHREAD_EXPLICIT_SCHED:使用 attr 中显式设置的调度参数。
2.调度属性继承机制
这两个函数控制的核心是线程创建时调度参数的来源:
2.1默认继承模式(PTHREAD_INHERIT_SCHED)
参数来源:新线程的调度策略(如 SCHED_OTHER、SCHED_FIFO)和优先级直接继承自创建它的线程。
属性忽略:此时通过 pthread_attr_setschedpolicy() 和 pthread_attr_setschedparam() 设置的参数会被忽略。
2.2 显式设置模式(PTHREAD_EXPLICIT_SCHED)
参数来源:新线程使用 attr 对象中显式设置的调度策略和优先级。
必要步骤:
调用 pthread_attr_setinheritsched(attr, PTHREAD_EXPLICIT_SCHED)。
通过 pthread_attr_setschedpolicy() 设置调度策略。
通过 pthread_attr_setschedparam() 设置优先级。
3.典型应用场景
3.1 需要精确控制线程优先级的场景
// 示例:创建一个高优先级的实时线程
pthread_attr_t attr;
struct sched_param param;
pthread_attr_init(&attr);
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED); // 显式设置模式
pthread_attr_setschedpolicy(&attr, SCHED_RR); // 实时轮转调度
param.sched_priority = 50; // 优先级50
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&thread, &attr, thread_function, NULL);
3.2 继承父线程调度参数的场景
// 示例:创建一个继承父线程调度策略的线程
pthread_attr_t attr;
pthread_attr_init(&attr);
// 未调用pthread_attr_setinheritsched(),默认继承父线程参数
// 此时设置的调度策略无效
pthread_attr_setschedpolicy(&attr, SCHED_FIFO); // 被忽略
pthread_create(&thread, &attr, thread_function, NULL);
4.注意事项
4.1 权限与资源限制
实时调度策略(如 SCHED_FIFO、SCHED_RR)需要 root 权限或 CAP_SYS_NICE 能力。
普通用户只能使用 SCHED_OTHER 策略,并通过 nice 值(-20~19)间接调整优先级。
4.2 优先级范围差异
调度策略 优先级范围 设置方式
SCHED_OTHER nice 值 (-20~19) 通过 sched_param.sched_nice
SCHED_FIFO 实时优先级 (1~99) 通过 sched_param.sched_priority
SCHED_RR 实时优先级 (1~99) 同上
4.3 函数返回值
成功时返回 0,失败时返回错误码(如 EINVAL 参数无效)。
5.完整示例代码
以下代码演示了两种继承模式的差异:
#include <pthread.h>
#include <stdio.h>
#include <sched.h>
#include <stdlib.h>
void print_sched_info(const char* prefix) {
int policy;
struct sched_param param;
pthread_getschedparam(pthread_self(), &policy, ¶m);
printf("%s 调度策略: ", prefix);
switch (policy) {
case SCHED_FIFO: printf("SCHED_FIFO\n"); break;
case SCHED_RR: printf("SCHED_RR\n"); break;
case SCHED_OTHER: printf("SCHED_OTHER\n"); break;
default: printf("未知策略\n"); break;
}
printf("%s 优先级: %d\n", prefix, param.sched_priority);
}
// 显式设置调度参数的线程
void* explicit_thread(void* arg) {
print_sched_info("显式模式线程");
return NULL;
}
// 继承父线程调度参数的线程
void* inherit_thread(void* arg) {
print_sched_info("继承模式线程");
return NULL;
}
int main() {
pthread_t tid_explicit, tid_inherit;
pthread_attr_t attr;
struct sched_param param;
// 设置主线程为高优先级(需要root权限)
param.sched_priority = 30;
pthread_setschedparam(pthread_self(), SCHED_RR, ¶m);
printf("主线程调度信息:\n");
print_sched_info("主线程");
// 创建显式模式线程
pthread_attr_init(&attr);
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
param.sched_priority = 50;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid_explicit, &attr, explicit_thread, NULL);
pthread_attr_destroy(&attr);
// 创建继承模式线程
pthread_attr_init(&attr);
// 默认是PTHREAD_INHERIT_SCHED,无需显式调用
pthread_create(&tid_inherit, &attr, inherit_thread, NULL);
pthread_attr_destroy(&attr);
// 等待线程结束
pthread_join(tid_explicit, NULL);
pthread_join(tid_inherit, NULL);
return 0;
}
6.总结
pthread_attr_setinheritsched():决定新线程的调度参数是继承自父线程还是显式设置。
pthread_attr_getinheritsched():查询当前线程属性的继承模式。
最佳实践:
需要精确控制线程调度特性时,使用 PTHREAD_EXPLICIT_SCHED 并显式设置参数。
需要线程间保持一致的调度策略时,使用默认的 PTHREAD_INHERIT_SCHED。
设置实时调度策略时,确保程序具有相应权限。
2、动态优先级
动态优先级指的是当多个普通任务并发运行时,系统会根据其实际运行情况实时调整优先级,目的是实现公平调度和优化系统响应性。与静态优先级不同,动态优先级会随线程的执行情况而变化。
注意:动态优先级的值越高,优先级越低。
动态优先级调整的 API
1.运行时修改优先级
#include <pthread.h>
// 获取当前线程调度参数
int policy;
struct sched_param param;
pthread_getschedparam(pthread_self(), &policy, ¶m);
// 修改优先级
param.sched_priority = 70; // 实时优先级(1-99)
pthread_setschedparam(pthread_self(), policy, ¶m);
2.动态调整 nice 值
#include <unistd.h>
// 获取当前nice值
int old_nice = nice(0);
// 增加nice值(降低优先级)
nice(5);
四、线程信号响应
1.发送信号给线程
pthread_kill() 和 pthread_sigqueue() 是用于向特定线程发送信号的两个重要函数。
1.pthread_kill() 函数
函数原型
#include <pthread.h>
int pthread_kill(pthread_t thread, int sig);
参数:
thread:目标线程的 ID(由 pthread_create() 返回)。
sig:要发送的信号编号(如 SIGUSR1、SIGINT)。
返回值:
成功:返回 0。
失败:返回错误码(如 ESRCH 线程不存在)。
核心功能
向指定线程发送信号,信号处理规则:
若信号未被线程掩码阻塞,由目标线程处理。
若信号被阻塞,暂存于线程的待处理信号队列中,掩码解除后处理。
若信号处理函数为 SIG_DFL(默认),可能终止整个进程(如 SIGKILL)。
示例代码
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void* thread_function(void* arg) {
printf("子线程运行中...\n");
pause(); // 等待信号
printf("子线程收到信号,退出\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_function, NULL);
// 主线程休眠2秒,确保子线程已启动
sleep(2);
// 向子线程发送SIGUSR1信号
pthread_kill(tid, SIGUSR1);
// 等待子线程结束
pthread_join(tid, NULL);
return 0;
}
2.pthread_sigqueue() 函数
函数原型
#include <pthread.h>
int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);
参数:
thread:目标线程 ID。
sig:信号编号(推荐使用实时信号,如 SIGRTMIN)。
value:传递的参数,类型为 union sigval:
union sigval {
int sival_int; // 整数值
void* sival_ptr; // 指针值
};
返回值:
成功:返回 0。
失败:返回错误码。
核心功能
向指定线程发送带参数的信号,主要用于实时信号(32-64)。
信号参数可在信号处理函数中通过 siginfo_t 结构体获取:
void signal_handler(int signum, siginfo_t* info, void* context) {
printf("收到信号: %d,携带参数: %d\n", signum, info->si_value.sival_int);
}
示例代码
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
// 带参数的信号处理函数
void signal_handler(int signum, siginfo_t* info, void* context) {
printf("线程 %lu 收到信号 %d,携带参数: %d\n",
(unsigned long)pthread_self(), signum, info->si_value.sival_int);
}
void* thread_function(void* arg) {
// 设置信号处理函数(需使用sigaction并设置SA_SIGINFO标志)
struct sigaction sa;
sa.sa_sigaction = signal_handler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGRTMIN, &sa, NULL);
printf("子线程运行中,等待信号...\n");
while (1) pause();
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_function, NULL);
// 主线程休眠2秒,确保子线程已设置信号处理函数
sleep(2);
// 向子线程发送带参数的实时信号
union sigval sv;
sv.sival_int = 42; // 传递整数参数
pthread_sigqueue(tid, SIGRTMIN, sv);
// 等待子线程(实际不会结束,需手动终止)
pthread_join(tid, NULL);
return 0;
}
3.两函数对比与适用场景
| 特性 | pthread_kill() | pthread_sigqueue() |
|---|---|---|
| 信号参数 | 无 | 支持传递整数或指针参数 |
| 信号类型 | 适用于所有信号 | 推荐用于实时信号(32-64) |
| 信号排队 | 标准信号可能丢失,实时信号排队 | 实时信号保证排队,不丢失 |
| 处理函数接口 | void handler(int signum) | void handler(int, siginfo_t, void) |
| 典型场景 | 简单通知(如唤醒线程) | 需传递上下文的复杂通知 |
2.线程对信号屏蔽
在 Linux 多线程编程中,pthread_sigmask() 是一个用于操作线程信号掩码的关键函数。它允许线程独立控制哪些信号可以被接收,哪些信号应该被阻塞,从而实现更精细的信号管理。
1.函数原型与参数
#include <pthread.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明:
how:操作类型,可选值:
SIG_BLOCK:将 set 中的信号添加到当前掩码(阻塞这些信号)。
SIG_UNBLOCK:从当前掩码中移除 set 中的信号(解除阻塞)。
SIG_SETMASK:将当前掩码设置为 set。
set:指向信号集的指针,包含要操作的信号。
oldset:若不为 NULL,则存储修改前的信号掩码。
返回值:
成功:返回 0。
失败:返回错误码(如 EINVAL 参数无效)。
2.核心功能与特性
-
线程级信号掩码控制
每个线程有独立的信号掩码,互不影响。
新线程默认继承创建它的线程的信号掩码,但可通过 pthread_sigmask() 修改。 -
信号阻塞与挂起
被阻塞的信号不会被线程接收,而是暂时挂起。
当信号掩码解除对该信号的阻塞时,线程将处理挂起的信号。 -
与 sigprocmask() 的区别
sigprocmask():操作进程级信号掩码,影响所有线程。
pthread_sigmask():操作线程级信号掩码,仅影响当前线程。
3.信号集操作辅助函数
在使用 pthread_sigmask() 前,需通过以下函数初始化信号集:
#include <signal.h>
sigset_t set; // 定义信号集
sigemptyset(&set); // 清空信号集
sigfillset(&set); // 填充所有信号
sigaddset(&set, SIGINT); // 添加SIGINT到信号集
sigdelset(&set, SIGTERM); // 从信号集移除SIGTERM
sigismember(&set, SIGUSR1); // 检查SIGUSR1是否在信号集中
4.典型应用场景
- 防止信号干扰关键代码段
void critical_section() {
sigset_t mask, old_mask;
// 阻塞SIGINT和SIGTERM
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
pthread_sigmask(SIG_BLOCK, &mask, &old_mask);
// 关键代码段(不会被SIGINT/SIGTERM中断)
...
// 恢复原信号掩码
pthread_sigmask(SIG_SETMASK, &old_mask, NULL);
}
- 创建专用信号处理线程
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
void* signal_handler(void* arg) {
sigset_t *mask = (sigset_t*)arg;
int sig;
while (1) {
// 等待信号(自动解除阻塞并处理)
sigwait(mask, &sig);
switch (sig) {
case SIGINT:
printf("收到SIGINT,准备退出\n");
exit(0);
case SIGUSR1:
printf("收到SIGUSR1,执行特殊操作\n");
break;
}
}
return NULL;
}
int main() {
pthread_t tid;
sigset_t mask;
// 初始化信号掩码,阻塞SIGINT和SIGUSR1
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGUSR1);
// 主线程阻塞信号
pthread_sigmask(SIG_BLOCK, &mask, NULL);
// 创建专用信号处理线程
pthread_create(&tid, NULL, signal_handler, &mask);
pthread_detach(tid); // 分离线程
// 主线程继续执行其他任务
while (1) {
// 主业务逻辑
}
return 0;
}
- 线程安全的信号处理
// 线程函数中设置独立的信号掩码
void* worker_thread(void* arg) {
// 阻塞所有实时信号,避免干扰
sigset_t mask;
sigemptyset(&mask);
for (int i = SIGRTMIN; i <= SIGRTMAX; i++) {
sigaddset(&mask, i);
}
pthread_sigmask(SIG_BLOCK, &mask, NULL);
// 线程主逻辑
...
}
5.注意事项
- 信号处理函数的安全性
信号处理函数中只能调用异步信号安全函数(如 write()、_exit())。
避免调用非安全函数(如 printf()、malloc()),可能导致竞态条件。 - 实时信号与标准信号的差异
标准信号(1-31):不支持排队,多次发送可能只处理一次。
实时信号(32-64):支持排队,不会丢失。 - 信号掩码的继承
新线程默认继承父线程的信号掩码,若需不同掩码,需在线程函数中显式设置。 - 权限限制
阻塞 SIGKILL 和 SIGSTOP 无效,这两个信号无法被阻塞。
6.两个函数的区别
| 特性 | sigprocmask() | pthread_sigmask() |
|---|---|---|
| 作用范围 | 当前线程(但信号掩码继承自进程) | 仅修改调用线程的信号掩码 |
| 线程安全性 | 非线程安全(多线程调用可能冲突) | 线程安全 |
| 多线程默认行为 | 所有线程共享相同的信号掩码 | 每个线程独立维护信号掩码 |
| 适用场景 | 单线程程序或需要统一信号处理的多线程程序 | 多线程程序中各线程需要独立控制信号 |

浙公网安备 33010602011771号