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

引言

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


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

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

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

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

一、读写锁的概念

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

读写锁的适用场景:

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

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

二、读写锁的核心规则

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

三、读写锁的接口

cpp 复制代码
#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);

四、读写锁示例

cpp 复制代码
#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 唤醒等待队列中的所有线程

二、条件变量接口

cpp 复制代码
#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. 唤醒安全:被唤醒时线程会重新加锁,避免与其他线程操作冲突

执行流程:

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

cpp 复制代码
#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函数内部使用静态变量记录分割位置

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

问题示例:

cpp 复制代码
// 错误示例:多线程调用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,通过额外参数(指针地址)独立记录分割位置。

cpp 复制代码
// 线程安全版本
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 ...

二、信号量解决方案

cpp 复制代码
#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;
}

同步逻辑:

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

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


总结

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

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

二、读写锁核心规则

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

三、条件变量使用规范

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

四、线程安全函数

非安全函数 安全版本 区别
strtok strtok_r 增加saveptr参数
rand rand_r 增加种子参数
localtime localtime_r 结果存入党参

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

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

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

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

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

面试高频考点:

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

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

  • strtokstrtok_r的区别

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

学习建议:

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

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

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

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

相关推荐
其实防守也摸鱼6 小时前
CTF密码学综合教学指南--第二章
开发语言·网络·python·安全·网络安全·密码学·ctf
jimy16 小时前
C 语言的 static 关键字作用
c语言·开发语言·算法
枫叶丹47 小时前
【HarmonyOS 6.0】Camera Kit白平衡API深度解析:让三方应用真正“掌控”色彩
开发语言·华为·harmonyos·视频编解码
xyq20247 小时前
C# 运算符重载
开发语言
计算机安禾7 小时前
【Linux从入门到精通】第42篇:深入理解Linux内存管理
android·linux·运维
echome8887 小时前
Python 生成器与 yield 关键字实战:5 个节省内存的高级用法与性能优化技巧
开发语言·python
艾莉丝努力练剑7 小时前
【Linux网络】Linux 网络编程入门:UDP Socket 编程(上)
linux·运维·服务器·网络·c++·udp
码界筑梦坊7 小时前
112-基于Flask的游戏行业销售数据可视化分析系统
开发语言·python·游戏·信息可视化·flask·毕业设计·echarts
代码中介商7 小时前
Linux多线程编程完全指南:线程同步、互斥锁与生产者消费者模型
linux·运维·服务器