Linux 四大进程/线程同步锁详解:互斥锁、读写锁、条件变量、文件锁

前言

在 Linux 并发编程中,多线程、多进程共享资源竞争是最核心的问题。如果没有同步保护,会出现数据覆盖、逻辑错乱、日志乱序、库存超卖等一系列偶现 Bug。

日常开发中最常用的四大同步工具:互斥锁、读写锁、条件变量、文件锁,各自适配不同业务场景,很多开发者容易混淆用法、踩坑死锁、性能退化问题。

本文将统一标准化讲解四大机制:是什么、解决什么问题、全套函数参数解析、最小可运行代码、核心误区,看完即可彻底掌握 Linux 主流同步方案,适配面试、开发、排查问题。


一、互斥锁(Mutex)------ 最通用的排他锁

1. 核心概念 & 作用

定义 :互斥锁是 POSIX 标准的独占式同步锁,同一时刻仅允许**一个执行流(线程/进程)**持有锁、进入临界区。

解决痛点:解决多线程/跨进程共享资源的并发竞态问题,保证临界区代码串行执行,是通用性最强、最稳定的同步方案。

核心特性:排他访问、阻塞等待、简单易用、适配绝大多数临界区场景。

2. 全套函数声明 & 参数详解

cpp 复制代码
#include <pthread.h>

// 1. 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

// 2. 阻塞加锁:拿不到锁则线程休眠阻塞
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 3. 非阻塞加锁:拿不到锁直接返回失败,不阻塞
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);

参数解析

  • mutex:互斥锁变量指针,操作的目标锁;

  • attr:锁属性结构体,传 NULL 代表使用默认属性(线程共享、普通锁);

返回值 :所有函数成功返回 0,失败返回非 0 错误码。

初始化方式 :全局锁可直接静态初始化pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

3. 最小可运行代码示例

实现多线程计数器同步,解决并发计数错乱问题:

cpp 复制代码
#include <stdio.h>
#include <pthread.h>

// 定义互斥锁
pthread_mutex_t mutex;
int count = 0;

// 线程执行函数:循环计数
void *count_task(void *arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);   // 加锁:进入临界区
        count++;                      // 共享资源操作
        pthread_mutex_unlock(&mutex); // 解锁:退出临界区
    }
    return NULL;
}

int main() {
    // 初始化锁
    pthread_mutex_init(&mutex, NULL);

    pthread_t t1, t2;
    // 创建两个并发线程
    pthread_create(&t1, NULL, count_task, NULL);
    pthread_create(&t2, NULL, count_task, NULL);

    // 等待线程结束
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("最终计数结果:%d\n", count); // 固定输出 200000

    // 销毁锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

编译gcc mutex_demo.c -o mutex_demo -pthread

运行./mutex_demo

4. 核心注意事项

  • 禁止加锁后不解锁、重复解锁:忘解锁会导致线程永久阻塞,重复解锁会直接触发程序崩溃;

  • 避免锁粒度过大:临界区包含无关代码、IO 操作,会大幅降低并发性能;

  • 存在优先级反转问题,高优先级线程可能被低优先级线程阻塞,实时系统需配置优先级继承属性。


二、读写锁(RWLock)------ 读多写少场景性能神器

1. 核心概念 & 作用

定义 :读写锁是优化版的互斥锁,核心规则:读共享、写排他

解决痛点 :普通互斥锁读写完全互斥,多读场景下并发极低。读写锁支持多线程同时读、写操作独占,大幅提升读多写少场景的并发吞吐量。

核心特性:读无冲突共享、写独占阻塞、适配配置读取、缓存查询场景。

2. 全套函数声明 & 参数详解

cpp 复制代码
#include <pthread.h>

// 1. 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

// 2. 加读锁:多线程可同时加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

// 3. 加写锁:独占加锁,读写全部阻塞
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

// 4. 释放锁(读写锁通用)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

// 5. 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

参数解析

  • rwlock:读写锁变量指针;

  • attr:锁属性,NULL 为默认读优先属性;

返回值:成功返回 0,失败返回错误码。

3. 最小可运行代码示例

模拟多线程读、单线程写的缓存场景:

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_rwlock_t rwlock;
int cache_data = 100;

// 读线程:并发读取数据
void *read_task(void *arg) {
    int idx = *(int *)arg;
    while (1) {
        pthread_rwlock_rdlock(&rwlock);
        printf("读线程%d:读取缓存数据 = %d\n", idx, cache_data);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return NULL;
}

// 写线程:定时修改数据
void *write_task(void *arg) {
    while (1) {
        pthread_rwlock_wrlock(&rwlock);
        cache_data++;
        printf("写线程:更新缓存数据 = %d\n", cache_data);
        pthread_rwlock_unlock(&rwlock);
        sleep(3);
    }
    return NULL;
}

int main() {
    pthread_rwlock_init(&rwlock, NULL);

    pthread_t t1, t2, t3;
    int a=1, b=2;
    pthread_create(&t1, NULL, read_task, &a);
    pthread_create(&t2, NULL, read_task, &b);
    pthread_create(&t3, NULL, write_task, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);

    pthread_rwlock_destroy(&rwlock);
    return 0;
}

编译gcc rwlock_demo.c -o rwlock_demo -pthread

运行./rwlock_demo

4. 核心注意事项

  • 默认读优先,存在写者饥饿问题:持续大量读请求会导致写线程永久阻塞,高频更新场景需改为写优先锁属性;

  • 写多场景不适用:读写锁维护开销高于普通互斥锁,写频繁场景用互斥锁性能更好。


三、条件变量(Condition Variable)------ 线程时序同步神器

1. 核心概念 & 作用

定义 :条件变量必须配合互斥锁使用,是专门用于线程等待特定条件成立的同步机制。

解决痛点:互斥锁只能解决互斥访问,无法解决「等待条件」问题。条件变量可以让线程条件不满足时休眠等待,条件达成后唤醒,避免空轮询耗 CPU,是生产者-消费者模型的标准实现方案。

核心特性:阻塞等待、主动唤醒、有序执行、零空耗 CPU。

2. 全套函数声明 & 参数详解

cpp 复制代码
#include <pthread.h>

// 1. 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

// 2. 阻塞等待条件:自动解锁、休眠;唤醒后自动加锁
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

// 3. 唤醒一个等待线程
int pthread_cond_signal(pthread_cond_t *cond);

// 4. 唤醒所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);

// 5. 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);

参数解析

  • cond:条件变量指针;

  • mutex:配套绑定的互斥锁,必须非空;

  • attr:属性结构体,NULL 为默认属性。

3. 最小可运行代码示例

极简生产者-消费者模型:

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

// 消费者线程:等待生产完成
void *consumer(void *arg) {
    pthread_mutex_lock(&mutex);
    // 必须while规避虚假唤醒
    while (ready == 0) {
        pthread_cond_wait(&cond, &mutex);
    }
    printf("消费者:收到数据,开始消费\n");
    ready = 0;
    pthread_mutex_unlock(&mutex);
    return NULL;
}

// 生产者线程:生产后唤醒消费者
void *producer(void *arg) {
    sleep(1); // 模拟生产耗时
    pthread_mutex_lock(&mutex);
    ready = 1;
    printf("生产者:数据生产完成,唤醒消费者\n");
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, consumer, NULL);
    pthread_create(&t2, NULL, producer, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    pthread_cond_destroy(&cond);
    return 0;
}

编译gcc cond_demo.c -o cond_demo -pthread

运行 ./cond_demo

4. 核心注意事项

  • 禁止使用 if 判断条件,必须用 while:Linux 存在虚假唤醒(无信号自动唤醒),while 可二次校验条件,杜绝逻辑 Bug;

  • 条件变量不能单独使用:必须绑定互斥锁,否则会出现线程竞争、程序崩溃。


四、文件锁(fcntl)------ 跨进程文件同步专属方案

1. 核心概念 & 作用

定义 :fcntl 文件锁是 Linux 系统原生的文件级同步机制,支持对文件任意区间加读锁/写锁。

解决痛点 :上述三种锁仅适用于线程同步 ,无法跨进程生效。文件锁专门解决多进程并发读写同一文件的错乱、覆盖问题(日志写入、配置更新、进程通信)。

核心特性:跨进程生效、支持区间细粒度锁、分为建议锁/强制锁。

2. 全套函数声明 & 参数详解

cpp 复制代码
#include <fcntl.h>
#include <unistd.h>

int fcntl(int fd, int cmd, struct flock *lock);

参数解析

  • fd:已打开的文件描述符;

  • cmd:锁操作指令:

    • F_SETLK:非阻塞加锁,失败直接返回;

    • F_SETLKW:阻塞加锁,拿不到锁则等待;

  • lock:锁属性结构体:

    • l_type:锁类型 F_RDLCK(读锁)、F_WRLCK(写锁)、F_UNLCK(解锁);

    • l_start:文件锁定起始偏移;

    • l_len:锁定区间长度,0 代表锁定到文件末尾。

3. 最小可运行代码示例

多进程安全写入日志文件,避免内容错乱:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

// 文件加写锁函数
void file_wlock(int fd) {
    struct flock lock;
    lock.l_type = F_WRLCK;
    lock.l_start = 0;
    lock.l_len = 0;
    fcntl(fd, F_SETLKW, &lock);
}

// 文件解锁函数
void file_unlock(int fd) {
    struct flock lock;
    lock.l_type = F_UNLCK;
    lock.l_start = 0;
    lock.l_len = 0;
    fcntl(fd, F_SETLK, &lock);
}

int main() {
    int fd = open("log.txt", O_RDWR|O_CREAT|O_APPEND, 0666);
    if (fd < 0) exit(-1);

    // 模拟多进程写入
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程写入
        file_wlock(fd);
        write(fd, "子进程日志写入\n", strlen("子进程日志写入\n"));
        sleep(1);
        file_unlock(fd);
    } else {
        // 父进程写入
        file_wlock(fd);
        write(fd, "父进程日志写入\n", strlen("父进程日志写入\n"));
        sleep(1);
        file_unlock(fd);
    }

    close(fd);
    return 0;
}

编译gcc filelock_demo.c -o filelock_demo

运行./filelock_demo

4. 核心注意事项

  • 默认是建议锁:仅遵守锁规则的进程生效,恶意进程可直接绕过锁修改文件;如需强制锁需开启文件特殊权限;

  • 文件关闭自动解锁:进程退出、文件关闭后锁会自动释放,不适合长期持有锁的业务场景。


五、四大锁场景选型总结

同步机制 适用范围 核心优势 典型场景
互斥锁 多线程/跨进程 通用稳定、简单可靠 通用临界区、计数更新、状态修改
读写锁 多线程 读并发性能极高 配置读取、缓存查询、读多写少业务
条件变量 多线程 无空耗、精准时序同步 生产者消费者、线程等待唤醒
文件锁 多进程 跨进程生效、细粒度锁 多进程日志、配置文件读写

结尾

四大同步锁覆盖了 Linux 90% 以上的并发同步场景:线程互斥用互斥锁、读多写少用读写锁、时序等待用条件变量、跨进程文件同步用文件锁。

掌握函数原型+代码实战+避坑点,可以彻底解决开发中绝大多数竞态、死锁、性能问题,也是面试并发编程的核心考点。

相关推荐
摇滚侠4 分钟前
SpringMVC 入门到实战 SpringMVC 的执行流程 96
java·后端·spring·maven·intellij-idea
布朗克16819 分钟前
38 Spring Boot入门——自动配置、核心注解与Starter机制
java·spring boot·后端
月巴月巴白勺合鸟月半23 分钟前
在Linux下开发桌面程序
linux·运维·服务器
zh路西法24 分钟前
【tmux入门】终端分屏、SSH远程守护与一键启动脚本
linux·运维·ssh·bash
程序员老申24 分钟前
外呼突然全挂了,追查 24 分钟后我发现了 etcd 最阴的一颗雷
后端·程序员
何以解忧,唯有..24 分钟前
Go语言变量的声明方式详解
开发语言·后端·golang
长栎25 分钟前
MyBatis 缓存为啥总是失效?装饰器模式套娃的代价
后端
LuminousCPP25 分钟前
数据结构 - 单链表第一篇:单链表基础操作
c语言·数据结构·经验分享·笔记·学习
bright_ye27 分钟前
setjmp & longjmp 深度详解 + 代码示例
后端
To_OC27 分钟前
我一直以为 Ajax 是个黑盒,直到我写了这 50 行代码
前端·后端·全栈