news 2026/5/2 15:47:33

Linux多线程编程完全指南(续):条件变量、读写锁与线程安全函数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux多线程编程完全指南(续):条件变量、读写锁与线程安全函数

引言

在前面的文章中,我们学习了线程的创建、退出、等待,以及使用互斥锁和信号量解决线程同步问题。今天,我们将继续深入探讨多线程编程的另外两个重要同步机制:读写锁条件变量,以及多线程环境下的函数安全问题。


第一部分:线程同步方法回顾

Linux系统提供的线程同步方法主要有四种:

同步方法适用场景特点
互斥锁保护临界区同一时刻只有一个线程能持有锁
信号量资源计数、同步计数器,可控制多个资源
读写锁读多写少场景读锁共享,写锁独占
条件变量等待条件满足配合互斥锁使用,线程间通知机制

第二部分:读写锁(Read-Write Lock)

一、读写锁的概念

读写锁与互斥锁的区别在于:互斥锁加锁后其他线程无法再加锁,而读写锁在读多写少场景下允许多个读操作同时进行,但写操作必须互斥。

读写锁的适用场景:

  • 多个线程仅需读取共享数据(不修改)时,允许并发读操作

  • 涉及写操作时,必须独占访问,避免数据冲突

二、读写锁的核心规则

操作组合是否兼容说明
读锁 + 读锁✅ 兼容多个线程可同时持有读锁
读锁 + 写锁❌ 互斥持有写锁时禁止其他线程加读锁或写锁
写锁 + 写锁❌ 互斥同一时间仅允许一个写锁存在

三、读写锁的接口

#include <pthread.h> // 初始化读写锁 int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); // 加读锁(阻塞) int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 加写锁(阻塞) int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 解锁 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // 销毁读写锁 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); // 非阻塞版本 int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

四、读写锁示例

#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> pthread_rwlock_t rwlock; int shared_data = 100; // 读线程函数 void* reader(void* arg) { int id = *(int*)arg; pthread_rwlock_rdlock(&rwlock); printf("读线程%d: 开始读取数据...\n", id); sleep(1); printf("读线程%d: 读取到数据 = %d\n", id, shared_data); printf("读线程%d: 读结束\n", id); pthread_rwlock_unlock(&rwlock); return NULL; } // 写线程函数 void* writer(void* arg) { int id = *(int*)arg; pthread_rwlock_wrlock(&rwlock); printf("写线程%d: 开始写入数据...\n", id); shared_data = rand() % 1000; sleep(2); printf("写线程%d: 写入数据 = %d\n", id, shared_data); printf("写线程%d: 写结束\n", id); pthread_rwlock_unlock(&rwlock); return NULL; } int main() { pthread_t readers[3], writer_tid; int ids[3] = {1, 2, 3}; int wid = 1; // 初始化读写锁 pthread_rwlock_init(&rwlock, NULL); // 创建3个读线程 for (int i = 0; i < 3; i++) { pthread_create(&readers[i], NULL, reader, &ids[i]); } // 创建1个写线程 pthread_create(&writer_tid, NULL, writer, &wid); // 等待所有线程结束 for (int i = 0; i < 3; i++) { pthread_join(readers[i], NULL); } pthread_join(writer_tid, NULL); // 销毁读写锁 pthread_rwlock_destroy(&rwlock); return 0; }

运行结果特点:

  • 无锁时:读写操作交叉执行,存在数据竞争风险

  • 加锁后:读操作可并行,写操作严格串行,且读写操作互不干扰


第三部分:条件变量(Condition Variable)

一、条件变量的概念

条件变量是多线程编程中较抽象的概念。根据《高性能服务器编程》,条件变量用于线程间同步共享数据的值,其本质是提供线程间通知机制

当特定条件满足时,通过接口通知等待线程执行任务。

核心操作作用
pthread_cond_wait将线程加入等待队列并阻塞
pthread_cond_signal唤醒等待队列中的一个线程
pthread_cond_broadcast唤醒等待队列中的所有线程

二、条件变量接口

#include <pthread.h> // 初始化条件变量 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_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); // 唤醒一个等待线程 int pthread_cond_signal(pthread_cond_t *cond); // 唤醒所有等待线程 int pthread_cond_broadcast(pthread_cond_t *cond); // 销毁条件变量 int pthread_cond_destroy(pthread_cond_t *cond);

三、为什么pthread_cond_wait必须配合互斥锁?

pthread_cond_wait必须与互斥锁配合使用,原因如下:

  1. 原子性保护:wait操作包含将线程加入等待队列和释放锁两个步骤,需确保不可分割

  2. 唤醒安全:被唤醒时线程会重新加锁,避免与其他线程操作冲突

执行流程:

四、条件变量示例:控制线程输出顺序

#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> pthread_cond_t cond; pthread_mutex_t mutex; char buffer[256]; int done = 0; void* thread_func(void* arg) { char* name = (char*)arg; while (1) { pthread_mutex_lock(&mutex); pthread_cond_wait(&cond, &mutex); if (done) { pthread_mutex_unlock(&mutex); break; } printf("%s 读取到数据: %s\n", name, buffer); pthread_mutex_unlock(&mutex); } return NULL; } int main() { pthread_t t1, t2; pthread_cond_init(&cond, NULL); pthread_mutex_init(&mutex, NULL); pthread_create(&t1, NULL, thread_func, "线程A"); pthread_create(&t2, NULL, thread_func, "线程B"); while (1) { printf("请输入数据: "); fgets(buffer, sizeof(buffer), stdin); // 去除换行符 buffer[strlen(buffer) - 1] = '\0'; pthread_mutex_lock(&mutex); if (strcmp(buffer, "end") == 0) { done = 1; pthread_cond_broadcast(&cond); } else { pthread_cond_signal(&cond); } pthread_mutex_unlock(&mutex); if (done) break; } pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); return 0; }

关键点说明:

  • 所有唤醒操作需先加锁,避免与wait的队列操作冲突

  • pthread_cond_signal按需唤醒单个线程

  • pthread_cond_broadcast用于唤醒所有等待线程

  • 唤醒策略应根据业务需求选择


第四部分:线程安全函数

一、strtok函数的多线程问题

strtok函数用于字符串分割,通过分隔符将字符串拆分为多个字段。但它在多线程环境中存在严重问题。

问题原因:

  • strtok函数内部使用静态变量记录分割位置

  • 多线程并发调用时共享同一指针,引发数据覆盖

问题示例:

// 错误示例:多线程调用strtok void* thread_func1(void* arg) { char str[] = "abcde"; char* token = strtok(str, " "); // ... } void* thread_func2(void* arg) { char str[] = "12345"; char* token = strtok(str, " "); // ... } // 输出结果混杂:如"a1b2c3"

二、线程安全版本:strtok_r

解决方案:使用线程安全版本strtok_r,通过额外参数(指针地址)独立记录分割位置。

// 线程安全版本 char* strtok_r(char *str, const char *delim, char **saveptr); // 使用示例 void* thread_func(void* arg) { char buffer[] = "hello world from thread"; char* saveptr; char* token = strtok_r(buffer, " ", &saveptr); while (token != NULL) { printf("%s\n", token); token = strtok_r(NULL, " ", &saveptr); } return NULL; }

三、线程安全函数设计原则

原则说明
避免静态/全局变量或通过参数传递独立存储空间
识别线程安全版本strtok_r(后缀_r通常表示线程安全版本)
使用同步机制若必须共享状态,使用锁保护

系统库函数线程安全标识:

  • 后缀_r(reentrant)通常表示线程安全版本

  • rand_rlocaltime_rstrtok_r


第五部分:信号量控制线程输出顺序

一、经典问题:三个线程交替打印ABC

需求:三个线程分别输出字母A、B、C,要求严格按ABC顺序循环打印。

输出结果:A B C A B C A B C ...

二、信号量解决方案

#include <stdio.h> #include <pthread.h> #include <semaphore.h> #define LOOP_COUNT 10 sem_t sem_a, sem_b, sem_c; void* print_a(void* arg) { for (int i = 0; i < LOOP_COUNT; i++) { sem_wait(&sem_a); printf("A "); fflush(stdout); sem_post(&sem_b); } return NULL; } void* print_b(void* arg) { for (int i = 0; i < LOOP_COUNT; i++) { sem_wait(&sem_b); printf("B "); fflush(stdout); sem_post(&sem_c); } return NULL; } void* print_c(void* arg) { for (int i = 0; i < LOOP_COUNT; i++) { sem_wait(&sem_c); printf("C "); fflush(stdout); sem_post(&sem_a); } return NULL; } int main() { pthread_t t1, t2, t3; // 初始化信号量:sema=1, semb=0, semc=0 sem_init(&sem_a, 0, 1); sem_init(&sem_b, 0, 0); sem_init(&sem_c, 0, 0); pthread_create(&t1, NULL, print_a, NULL); pthread_create(&t2, NULL, print_b, NULL); pthread_create(&t3, NULL, print_c, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); sem_destroy(&sem_a); sem_destroy(&sem_b); sem_destroy(&sem_c); return 0; }

同步逻辑:

线程操作效果
线程AP(sem_a) → 打印A → V(sem_b)初始sem_a=1,先执行
线程BP(sem_b) → 打印B → V(sem_c)sem_b初始0,等待A唤醒
线程CP(sem_c) → 打印C → V(sem_a)sem_c初始0,等待B唤醒

关键点:初始值设置确保线程A优先执行,后续通过信号量链式触发,避免线程竞争导致乱序。


总结

一、四种线程同步机制对比

机制适用场景核心特点
互斥锁保护临界区独占访问
信号量资源计数可控制多个资源
读写锁读多写少读共享,写独占
条件变量等待条件满足通知机制,需配合互斥锁

二、读写锁核心规则

规则说明
读锁与读锁兼容(可并发)
读锁与写锁互斥
写锁与写锁互斥

三、条件变量使用规范

规则说明
必须配合互斥锁wait前必须加锁,wait内部会解锁
wait返回时已重新加锁被唤醒后自动重新获取锁
signal/broadcast前建议加锁保证等待队列状态稳定

四、线程安全函数

非安全函数安全版本区别
strtokstrtok_r增加saveptr参数
randrand_r增加种子参数
localtimelocaltime_r结果存入党参

本文介绍了多线程编程的另外两个重要同步机制:

  1. 读写锁:读多写少场景下提供更好的并发性能

  2. 条件变量:实现线程间的通知机制,配合互斥锁使用

  3. 线程安全函数:如何识别和使用线程安全版本的库函数

  4. 信号量控制输出顺序:经典的ABC交替打印问题

面试高频考点:

  • 读写锁与互斥锁的区别及适用场景

  • 条件变量必须配合互斥锁的原因

  • strtokstrtok_r的区别

  • 使用信号量控制线程执行顺序的方法

学习建议:

  1. 理解读写锁的兼容规则,动手修改代码观察效果

  2. 掌握条件变量的标准使用模式(加锁→wait→检查条件→解锁)

  3. 编写ABC交替打印代码,加深对信号量同步的理解

  4. 注意区分线程安全和非线程安全函数,避免踩坑

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/2 15:44:23

避开这3个坑,轻松下载NREL Wind Toolkit风速数据(新手避雷指南)

避开这3个坑&#xff0c;轻松下载NREL Wind Toolkit风速数据&#xff08;新手避雷指南&#xff09; 第一次接触NREL的风速数据下载&#xff0c;很多人会直接搜索教程按部就班操作&#xff0c;结果往往卡在某个环节无法继续。本文将聚焦三个最常见却最容易被忽视的"雷区&qu…

作者头像 李华
网站建设 2026/5/2 15:41:34

如何快速提升《鸣潮》游戏体验:WaveTools工具箱完整使用指南

如何快速提升《鸣潮》游戏体验&#xff1a;WaveTools工具箱完整使用指南 【免费下载链接】WaveTools &#x1f9f0;鸣潮工具箱 项目地址: https://gitcode.com/gh_mirrors/wa/WaveTools 你是否在玩《鸣潮》时遇到帧率不稳、画质不够清晰&#xff0c;或者想要更好地管理多…

作者头像 李华
网站建设 2026/5/2 15:38:24

tttLRM:测试时训练与3D高斯泼溅的革新结合

1. 项目概述&#xff1a;tttLRM如何革新3D重建技术在计算机视觉领域&#xff0c;3D重建一直是个极具挑战性的任务。想象一下&#xff0c;你手头只有几张从不同角度拍摄的照片&#xff0c;却要还原出物体的完整三维结构——这就像侦探通过零星线索还原犯罪现场一样困难。传统方法…

作者头像 李华
网站建设 2026/5/2 15:36:24

AI快速开发工具包ai-fdl-kit:一站式解决AI工程化痛点

1. 项目概述&#xff1a;一个面向AI应用开发的“瑞士军刀”式工具包最近在GitHub上看到一个挺有意思的项目&#xff0c;叫ai-fdl-kit&#xff0c;作者是 Theuns Barnardt。光看名字&#xff0c;ai-fdl-kit&#xff0c;拆解一下就是“AI Fast Development Library Kit”&#xf…

作者头像 李华
网站建设 2026/5/2 15:32:26

N_m3u8DL-CLI-SimpleG:终极M3U8视频下载解决方案指南

N_m3u8DL-CLI-SimpleG&#xff1a;终极M3U8视频下载解决方案指南 【免费下载链接】N_m3u8DL-CLI-SimpleG N_m3u8DL-CLIs simple GUI 项目地址: https://gitcode.com/gh_mirrors/nm3/N_m3u8DL-CLI-SimpleG 在当今数字内容时代&#xff0c;你是否经常遇到想保存在线视频却…

作者头像 李华