ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux --读者写者问题、读写锁与自旋锁

Linux --读者写者问题、读写锁与自旋锁 为什么需要读者写者问题在多线程编程中同步是一个永恒的话题。我们之前接触过生产者-消费者问题它描述的是生产者往缓冲区放数据消费者从缓冲区取数据两者需要互斥地访问缓冲区同时还要在缓冲区空/满时进行等待和唤醒。而读者写者问题是另一类经典的同步问题它的场景是有一块共享数据比如一个文件、一个数据库、一块内存。多个读者可以同时读取这块数据因为读操作不会改变数据所以读者之间不需要互斥。写者在写入数据时必须独占访问因为写操作会改变数据如果此时有其他读者或写者在访问就会导致数据不一致。写者与读者之间、写者与写者之间必须互斥。简单来说读共享写独占。读者写者 vs 生产者消费者对比项生产者-消费者读者-写者核心矛盾缓冲区空/满时的等待与唤醒读与写、写与写之间的互斥互斥关系生产者与消费者互斥访问缓冲区写者与所有人互斥读者之间不互斥同步关系生产者等待缓冲区有空位消费者等待缓冲区有数据读者等待写者离开写者等待所有读者离开典型场景消息队列、任务队列文件读写、数据库读写、配置中心重点理解读者写者问题的核心在于如何协调读者和写者之间的同步使得读操作可以并发写操作必须独占同时还要避免某一方“饿死”。读者写者问题的伪代码公共部分uint32_t reader_count 0; // 当前正在读取的读者数量 lock_t count_lock; // 保护 reader_count 的锁 lock_t writer_lock; // 写者锁读者和写者共享reader_count记录当前有多少个读者正在读。count_lock因为多个读者可能同时修改reader_count所以需要一把锁来保护它。writer_lock写者需要持有的锁同时第一个读者进入时也要持有它最后一个读者离开时释放它。Reader读者// 加锁 lock(count_lock); if (reader_count 0) lock(writer_lock); // 第一个读者锁住写者锁 reader_count; unlock(count_lock); // read; // 解锁 lock(count_lock); --reader_count; if (reader_count 0) unlock(writer_lock); // 最后一个读者释放写者锁 unlock(count_lock);逻辑解读读者先锁住count_lock然后检查自己是不是第一个读者。如果是第一个读者reader_count 0说明当前没有读者在读但可能有写者在写所以需要获取writer_lock。如果写者正在写读者会在这里阻塞直到写者释放writer_lock。然后reader_count表示自己开始读了释放count_lock。读取数据。读完后再次锁住count_lockreader_count--。如果自己是最后一个读者reader_count 0说明所有读者都离开了此时需要释放writer_lock让等待的写者可以进入。释放count_lock。关键点第一个读者负责“锁住”写者最后一个读者负责“释放”写者。中间的读者只是简单地增加/减少计数不会触碰writer_lock。Writer写者lock(writer_lock); // write unlock(writer_lock);逻辑解读写者直接尝试获取writer_lock。如果此时有读者正在读writer_lock被第一个读者持有写者会阻塞。如果此时有其他写者在写写者也会阻塞。获取到锁后独占写入。写完释放锁。注意这个伪代码实现的是读者优先策略。因为只要有一个读者持有writer_lock后续的读者都可以直接进入它们只需要count_lock而写者必须等待所有读者离开。如果读者源源不断写者可能永远等待即写者饥饿。读写锁pthread_rwlock在实际编程中我们不需要自己手动实现上述逻辑POSIX 提供了读写锁pthread_rwlock_t它封装了读者写者的同步机制。读写锁的行为当前锁状态读锁请求写锁请求无锁可以可以读锁可以阻塞写锁阻塞阻塞总结写独占读共享读锁优先级高默认。读写锁的接口初始化与销毁int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);attr通常传NULL表示使用默认属性。使用前必须初始化使用后必须销毁。加锁与解锁int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 读锁 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 写锁 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // 解锁读锁多个线程可以同时持有读锁。写锁同一时刻只能有一个线程持有写锁且此时不能有读锁。解锁无论是读锁还是写锁都用同一个unlock。设置读写优先级int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);pref有三种选择选项含义PTHREAD_RWLOCK_PREFER_READER_NP读者优先默认可能导致写者饥饿PTHREAD_RWLOCK_PREFER_WRITER_NP写者优先但目前有 BUG表现和读者优先一致PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP写者优先但写者不能递归加锁注意写者优先可以缓解写者饥饿但可能导致读者饥饿。实际使用时需要根据场景权衡。读者优先 vs 写者优先读者优先只要有读者在读后续读者可以直接进入写者必须等待所有读者离开。可能导致写者饥饿。写者优先一旦有写者到达后续读者会被阻塞直到写者完成。可能导致读者饥饿。选择建议如果读操作非常频繁写操作很少且写者饥饿不是问题可以用读者优先。如果写操作也很重要不能长时间等待可以用写者优先注意PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP。自旋锁Spinlock什么是自旋锁自旋锁是一种忙等待的锁。当一个线程尝试获取自旋锁但锁已被占用时它不会进入休眠而是在一个循环中不断检查锁是否可用。一旦锁被释放线程立即获取。类比你去上厕所发现门锁着你不会回去睡觉而是站在门口一直问“好了没好了没”直到里面的人出来。自旋锁的原理自旋锁通常使用一个原子标志位来表示锁的状态false锁可用。true锁已被占用。获取锁时使用CASCompare-And-Swap原子操作while (atomic_flag_test_and_set(spinlock)) { // 忙等待 }atomic_flag_test_and_set如果标志位为false则设置为true并返回false表示获取成功如果为true则返回true表示获取失败。释放锁atomic_flag_clear(spinlock);将标志位设为false。自旋锁的优缺点优点低延迟不会让线程休眠避免了线程切换的开销。减少系统调度开销等待锁的线程不会被阻塞不需要上下文切换。缺点CPU 资源浪费如果锁持有时间较长等待线程会一直自旋浪费 CPU。可能引起活锁多个线程同时自旋如果没有退避策略可能都无法进入临界区。使用场景短暂等待锁被占用的时间很短比如简单的计数器操作。多线程锁使用通常用于系统底层同步多个 CPU 对共享资源的访问。不可睡眠的场景比如中断处理程序中不能休眠只能用自旋锁。Linux 提供的自旋锁系统调用#include pthread.h int pthread_spin_lock(pthread_spinlock_t *lock); int pthread_spin_trylock(pthread_spinlock_t *lock); int pthread_spin_unlock(pthread_spinlock_t *lock); int pthread_spin_init(pthread_spinlock_t *lock, int pshared); int pthread_spin_destroy(pthread_spinlock_t *lock);psharedPTHREAD_PROCESS_PRIVATE表示线程间共享PTHREAD_PROCESS_SHARED表示进程间共享。互斥锁 vs 自旋锁 vs 读写锁锁类型行为适用场景缺点互斥锁Mutex获取失败时休眠释放时唤醒锁持有时间较长竞争不激烈上下文切换开销自旋锁Spinlock获取失败时忙等待锁持有时间极短多核环境CPU 浪费可能活锁读写锁Rwlock读共享写独占多读少写可能读者/写者饥饿选择原则如果临界区执行时间短且 CPU 多核用自旋锁。如果临界区执行时间长或者单核用互斥锁。如果读多写少用读写锁。读者写者问题的变种读者优先只要有一个读者在读后续读者直接进入写者等待。写者优先一旦有写者等待后续读者阻塞写者优先进入。公平竞争读者和写者按到达顺序竞争避免饥饿。读写锁的实现细节读写锁的实现通常需要一个计数器和两个条件变量或信号量reader_count当前读者数量。writer_count当前写者数量通常为 0 或 1。mutex保护计数器。read_cond读者等待的条件变量。write_cond写者等待的条件变量。读者进入时如果写者正在写等待。否则reader_count进入读。写者进入时如果读者正在读或写者正在写等待。否则进入写。自旋锁的优化退避策略自旋失败后暂停一段时间再重试减少 CPU 浪费。队列自旋锁每个线程在一个队列中等待避免所有线程同时自旋。自适应自旋锁根据历史成功率动态调整自旋时间。实际应用中的建议避免锁的嵌套容易导致死锁。锁的粒度要小只保护必要的临界区。读写锁并不是解决所有并发问题的万能工具如果写操作也很频繁读写锁可能不如互斥锁。自旋锁不要用于单核单核自旋没有意义因为自旋的线程占着 CPU持有锁的线程无法运行。
返回列表