Linux 学习笔记(三十六):并发进阶——读写锁 vs 自旋锁,到底怎么选?

📌 核心结论(TL;DR):

  1. 选型口诀 :多读少写选读写锁 ;极短临界区+多核选自旋锁 ;复杂逻辑选互斥锁(Mutex)。

  2. 读写锁三大铁律:读读共享、读写互斥、写写互斥。

  3. 自旋锁致命缺点:如果锁被占用时间较长,线程一直原地"忙等待(自旋)",会白白把 CPU 跑满(浪费算力)。

  4. 饥饿问题:读写锁默认策略下,如果读者源源不断,写者可能会被"饿死"(长时间抢不到锁)。

一、 互斥锁的局限性

传统的互斥锁(Mutex)非常霸道:无论你是读取数据还是修改数据,只要有线程加锁,其他线程全部阻塞。但在实际开发中,往往是"多读少写"的场景。如果读操作也互斥,系统的并发性能将大打折扣。

二、 读写锁(Reader-Writer Lock)

读写锁专门针对多读少写场景设计,遵循三大核心规则:

  1. 读读共享:多个线程可以同时持有读锁。

  2. 读写互斥:写操作独占,此时不允许任何读操作。

  3. 写写互斥:多个写线程之间必须互斥。

核心 API:

  • pthread_rwlock_rdlock():加读锁。

  • pthread_rwlock_wrlock():加写锁。

  • pthread_rwlock_unlock():解锁。

(插入 rwlock_test.cc 代码)

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

int shared_data = 0;
pthread_rwlock_t rwlock;

void* Reader(void* arg) {
    long id = (long)arg;
    while (true) {
        pthread_rwlock_rdlock(&rwlock); // 加读锁
        std::cout << "读者-" << id << " 正在读取数据: " << shared_data << std::endl;
        sleep(1);
        pthread_rwlock_unlock(&rwlock); // 解锁
    }
    return nullptr;
}

void* Writer(void* arg) {
    long id = (long)arg;
    while (true) {
        pthread_rwlock_wrlock(&rwlock); // 加写锁
        shared_data++;
        std::cout << "写者-" << id << " 正在写入数据: " << shared_data << std::endl;
        sleep(1);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return nullptr;
}

三、 自旋锁(Spinlock)

自旋锁与 Mutex 的最大区别在于"等待"的方式:

  • Mutex 发现锁被占用,会让线程进入睡眠状态(阻塞态),让出 CPU。醒来时需要上下文切换,开销大。

  • Spinlock 发现锁被占用,会开启死循环原地自旋(忙等待),不放弃 CPU。

(插入 spinlock_test.cc 核心代码)

cpp 复制代码
pthread_spinlock_t spinlock; // 自旋锁

void* route(void* arg) {
    while (true) {
        pthread_spin_lock(&spinlock); // 加自旋锁
        if (ticket > 0) {
            // 极短的操作
            ticket--;
        } else {
            pthread_spin_unlock(&spinlock);
            break;
        }
        pthread_spin_unlock(&spinlock);
    }
}

四、 面试必考:如何避坑与选型?

  1. 自旋锁的坑:由于是在原地死循环,如果临界区代码很长,其他线程会白白消耗大量 CPU 资源(在单核 CPU 上尤为致命,因为自旋的线程占着 CPU,拿到锁的线程根本没法运行)。

  2. 读写锁的"写者饥饿" :在默认读者优先的策略下,如果一直有新的读者进来,写者可能永远无法获得写锁。可以通过设置 PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 属性让写者优先。

  3. 选型总结:

    • 读操作多、写操作少 →→ 读写锁。

    • 临界区只有几条指令,且是多核 CPU →→ 自旋锁。

    • 临界区长、逻辑复杂 →→ 互斥锁 (Mutex)。

自我实战操作

咱们来写一段代码,用读写锁模拟"2 个读者,2 个写者"同时访问一个共享数据。

bash 复制代码
cd ~/linux_study
mkdir rw_spin_test
cd rw_spin_test
实战 1:读写锁测试
bash 复制代码
vim rwlock_test.cc

(输入以下代码)

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

int shared_data = 0;      // 共享数据
pthread_rwlock_t rwlock;  // 读写锁

// 读者线程
void* Reader(void* arg) {
    long id = (long)arg;
    while (true) {
        pthread_rwlock_rdlock(&rwlock); // 加读锁
        std::cout << "读者-" << id << " 正在读取数据: " << shared_data << std::endl;
        sleep(1); // 模拟读取耗时
        pthread_rwlock_unlock(&rwlock); // 解锁
        // sleep(1); // 打开这个注释,写者会饿死
    }
    return nullptr;
}

// 写者线程
void* Writer(void* arg) {
    long id = (long)arg;
    while (true) {
        pthread_rwlock_wrlock(&rwlock); // 加写锁
        shared_data++;
        std::cout << "写者-" << id << " 正在写入数据: " << shared_data << std::endl;
        sleep(1); // 模拟写入耗时
        pthread_rwlock_unlock(&rwlock); // 解锁
        sleep(1); // 写者给读者留点机会
    }
    return nullptr;
}

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

    pthread_t r1, r2, w1, w2;
    pthread_create(&r1, nullptr, Reader, (void*)1);
    pthread_create(&r2, nullptr, Reader, (void*)2);
    pthread_create(&w1, nullptr, Writer, (void*)1);
    pthread_create(&w2, nullptr, Writer, (void*)2);

    pthread_join(r1, nullptr);
    pthread_join(r2, nullptr);
    pthread_join(w1, nullptr);
    pthread_join(w2, nullptr);

    pthread_rwlock_destroy(&rwlock);
    return 0;
}

编译运行:

bash 复制代码
g++ rwlock_test.cc -o rwlock_test -lpthread
./rwlock_test

👉 现象:你会看到"读者-1"和"读者-2"经常一起打印(读读共享),但当"写者"打印时,两个读者必定是停住的(读写互斥)。

实战 2:自旋锁测试

(注意:自旋锁在单核虚拟机上测试效果不明显,但代码必须要会写)

bash 复制代码
vim spinlock_test.cc

(输入以下代码)

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

int ticket = 1000;
pthread_spinlock_t spinlock; // 自旋锁

void* route(void* arg) {
    long id = (long)arg;
    while (true) {
        pthread_spin_lock(&spinlock); // 加自旋锁
        if (ticket > 0) {
            usleep(100); // 模拟极短的操作
            printf("线程-%ld 卖出票: %d\n", id, ticket);
            ticket--;
        } else {
            pthread_spin_unlock(&spinlock);
            break;
        }
        pthread_spin_unlock(&spinlock); // 解锁
    }
    return nullptr;
}

int main() {
    pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, route, (void*)1);
    pthread_create(&t2, nullptr, route, (void*)2);
    pthread_create(&t3, nullptr, route, (void*)3);

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

    pthread_spin_destroy(&spinlock);
    return 0;
}

(编译运行 g++ spinlock_test.cc -o spinlock_test -lpthread && ./spinlock_test)

相关推荐
qq_3646821536 分钟前
录音APP怎么切换语言?多语言版本如何使用
java·服务器·前端
正经教主41 分钟前
【FDE系列】阶段2:Day 43(UP详细版):Docker Compose — 多容器一键编排
运维·docker·容器
是隼人44 分钟前
buuctf-pwn [极客大挑战 2019]Not Bad(orw+jmp)题解(学习过程持续更新)
学习·安全·pwn入门·ctf入门
摇滚侠1 小时前
《On Java 中文版 基础卷》阅读笔记 复用 13
java·笔记·python
大耳朵爱学习1 小时前
Andrew Ng深度解析:普通人如何利用AI把想法变成工具,实现工作自动化(小白程序员必看)
运维·人工智能·ai·自动化·大模型·就业·大模型学习
木雷坞1 小时前
AnythingLLM Docker部署:中文资料上传后无引用的检查路径
运维·docker·容器
张赐荣1 小时前
争渡读屏OCR文字/图像识别插件接口功能实现详解
运维·服务器·ocr
中科三方1 小时前
AI时代新攻击面:悬空DNS记录正在成为黑客的低成本入侵入口
服务器·前端·人工智能
bu_shuo1 小时前
删除Windows资源管理器左侧【此电脑】的Linux企鹅图标
linux·windows