ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

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 代码)

#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 核心代码)

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 个写者”同时访问一个共享数据。

cd ~/linux_study mkdir rw_spin_test cd rw_spin_test
实战 1:读写锁测试
vim rwlock_test.cc

(输入以下代码)

#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; }

编译运行:

g++ rwlock_test.cc -o rwlock_test -lpthread ./rwlock_test

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

实战 2:自旋锁测试

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

vim spinlock_test.cc

(输入以下代码)

#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)

返回列表