ARTICLE DETAIL

资讯详情

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

Linux 线程同步:读写锁

Linux 线程同步:读写锁 Linux 线程同步读写锁课程尚硅谷《嵌入式 Linux 应用层开发》第 4 章线程处理依据2026-09-29 19:30 录音转写整理到15:02课程 PDF 第 150—158 页。本节边界讲清读写锁原理、基础 API、未加写锁的错误示例、加写锁后的修正以及读写线程执行顺序实验的准备。第 158 页开始的“写饥饿”对应录音 17:56 以后留到下一节。1. 为什么互斥锁之后还要学习读写锁互斥锁不区分读和写。只要一个线程持有互斥锁其他线程都要等待。但很多共享数据具有“读得多、写得少”的特点多个线程只读取同一份稳定数据时可以并发执行写线程修改数据时必须独占写入期间不能让读线程看到一半旧、一半新的状态。读写锁read-write lock用两种加锁方式表达这种规则读锁共享占用允许多个读者并发 写锁独占占用只允许一个写者进入2. 读锁与写锁的兼容关系当前状态新读者申请读锁新写者申请写锁没有线程持锁可以获得可以获得一个或多个线程持有读锁通常可继续获得读锁必须等待全部读锁释放一个线程持有写锁必须等待必须等待“通常可继续获得读锁”还受调度策略以及是否已有写者等待等因素影响不能据此假定读者永远优先。读否是写否是线程申请访问共享数据读还是写当前是否有写者持锁取得读锁阻塞等待读取共享数据释放读锁是否有任何读者或写者持锁取得写锁修改共享数据释放写锁3. “多个线程同时读没问题”的适用条件录音用“大家一起看已经写好的黑板”解释读锁。这个比喻成立需要满足读者只读取不修改对象在读取期间仍然存在没有写者绕过锁修改所有读者和写者使用同一把读写锁。如果共享数据可能被写线程修改读线程也必须获取读锁。否则读线程可能与写线程形成数据竞争或看到不一致的组合状态。4.pthread_rwlock_t的使用原则读写锁类型声明为#includepthread.hpthread_rwlock_trwlock;教材第 151 页展示了 glibc 内部的联合体定义。应用程序应把pthread_rwlock_t当作不透明类型只通过pthread.h使用不访问内部成员不复制读写锁对象给相关函数传入同一个对象的地址。5. 初始化与销毁5.1 静态初始化默认属性下可以在定义时初始化staticpthread_rwlock_trwlockPTHREAD_RWLOCK_INITIALIZER;这种方式不再调用pthread_rwlock_init()。5.2 动态初始化pthread_rwlock_trwlock;interrorpthread_rwlock_init(rwlock,NULL);第二个参数是属性对象。传NULL表示使用默认属性。5.3 销毁interrorpthread_rwlock_destroy(rwlock);销毁前必须确认所有使用它的线程已经结束或不再访问它当前没有读者或写者持锁不会再有线程等待这把锁。静态初始化只表示不需要调用pthread_rwlock_init()。如果锁与进程同寿命程序退出时系统会回收进程资源如果对象有独立、可重复的生命周期则应在安全时显式销毁。6. 本节五个核心 APIintpthread_rwlock_init(pthread_rwlock_t*restrict rwlock,constpthread_rwlockattr_t*restrict attr);intpthread_rwlock_rdlock(pthread_rwlock_t*rwlock);intpthread_rwlock_wrlock(pthread_rwlock_t*rwlock);intpthread_rwlock_unlock(pthread_rwlock_t*rwlock);intpthread_rwlock_destroy(pthread_rwlock_t*rwlock);函数作用获取不到时pthread_rwlock_rdlock获取读锁有写者阻挡时等待pthread_rwlock_wrlock获取写锁只要还有读者或写者就等待pthread_rwlock_unlock释放当前线程持有的一次读锁或写锁不适用同一把读写锁的读锁和写锁都用pthread_rwlock_unlock()释放。Pthreads 函数成功返回0否则返回错误号。基础检查写法interrorpthread_rwlock_rdlock(rwlock);if(error!0){fprintf(stderr,pthread_rwlock_rdlock: %s\n,strerror(error));}7. 第一个实验写操作没有加锁教材第 152—154 页故意让两个写线程修改同一个变量却不给写操作加锁staticintshared_data0;staticvoid*writer_without_lock(void*arg){inttempshared_data1;sleep(1);shared_datatemp;printf(%s 完成写入shared_data %d\n,(char*)arg,shared_data);returnNULL;}sleep(1)是为了放大两个写线程发生交错的机会步骤写线程 1写线程 2shared_data1读取 0算出temp102睡眠读取 0算出temp103睡眠04写回 115写回 11预期加两次得到 2实际可能得到 1。这与上一节number的丢失更新本质相同。7.1 只有读线程加读锁为什么仍然错误教材错误示例中的读线程这样写pthread_rwlock_rdlock(rwlock);printf(shared_data %d\n,shared_data);pthread_rwlock_unlock(rwlock);读线程之间可以互相协调但两个写线程完全没有使用这把锁。读写锁只有在所有访问者共同遵守时才有效。读者使用 rwlock 写者绕过 rwlock ↓ 同步协议不完整8. 修正实验写线程获取写锁教材第 154—156 页给写操作补上写锁staticvoid*lock_writer(void*arg){pthread_rwlock_wrlock(rwlock);inttempshared_data1;sleep(1);/* 仅为演示持锁期间其他线程会等待 */shared_datatemp;printf(%s 完成写入shared_data %d\n,(char*)arg,shared_data);pthread_rwlock_unlock(rwlock);returnNULL;}执行过程变成写线程 2读写锁写线程 1写线程 2读写锁写线程 1阻塞等待wrlock成功wrlockshared_data 由 0 变 1unlock获得写锁shared_data 由 1 变 2unlock两个写线程不再同时进入临界区最终值稳定为 2。9. 基础完整练习两个写者和三个读者下面的例子保留录音中的主要现象同时补上返回值检查。读者看到 0、1 或 2 都有可能因为调度顺序不确定但每次读取都发生在读锁保护下最终值应为 2。#includepthread.h#includestdio.h#includestdlib.h#includestring.h#includeunistd.hstaticintshared_data0;staticpthread_rwlock_trwlockPTHREAD_RWLOCK_INITIALIZER;staticvoidcheck_pthread(interror,constchar*operation){if(error!0){fprintf(stderr,%s: %s\n,operation,strerror(error));exit(EXIT_FAILURE);}}staticvoid*reader(void*arg){constchar*namearg;check_pthread(pthread_rwlock_rdlock(rwlock),pthread_rwlock_rdlock);printf(%s 读取%d\n,name,shared_data);sleep(1);/* 演示多个读者可以同时持有读锁 */check_pthread(pthread_rwlock_unlock(rwlock),pthread_rwlock_unlock);returnNULL;}staticvoid*writer(void*arg){constchar*namearg;check_pthread(pthread_rwlock_wrlock(rwlock),pthread_rwlock_wrlock);shared_data;printf(%s 写入%d\n,name,shared_data);check_pthread(pthread_rwlock_unlock(rwlock),pthread_rwlock_unlock);returnNULL;}intmain(void){pthread_tthreads[5];check_pthread(pthread_create(threads[0],NULL,reader,reader1),pthread_create);check_pthread(pthread_create(threads[1],NULL,reader,reader2),pthread_create);check_pthread(pthread_create(threads[2],NULL,writer,writer1),pthread_create);check_pthread(pthread_create(threads[3],NULL,reader,reader3),pthread_create);check_pthread(pthread_create(threads[4],NULL,writer,writer2),pthread_create);for(inti0;i5;i){check_pthread(pthread_join(threads[i],NULL),pthread_join);}printf(最终值%d\n,shared_data);check_pthread(pthread_rwlock_destroy(rwlock),pthread_rwlock_destroy);returnEXIT_SUCCESS;}编译运行gcc-Wall-Wextra-pthreadrwlock_basic.c-orwlock_basic ./rwlock_basic9.1 观察重点多运行几次输出顺序可能改变先创建的线程不保证先执行多个读者可以同时持有读锁写者必须等所有当前读者释放写者持锁时新的读者和其他写者都不能进入所有线程结束后最终值为 2。10. 为什么创建顺序不等于执行顺序教材第 156—158 页把第二个写线程放到多个读线程之间创建用来观察读写顺序创建 writer1 创建 reader1、reader2、reader3 创建 writer2 创建 reader4、reader5、reader6这只是向系统提交线程的先后顺序。线程何时真正获得 CPU、何时开始申请锁由调度器和当时的锁状态共同决定。因此不能写出这样的业务假设因为 reader1 先 pthread_create所以它一定先于 writer2 读取如果业务需要固定顺序应使用条件变量、信号量、队列或明确的join关系不能依赖创建顺序和sleep()。11.sleep()在录音示例中的作用录音使用sleep()有两个教学目的放大未加锁写操作的交错更容易观察丢失更新延长持有读锁或写锁的时间更容易看清其他线程是否被阻塞。sleep()不能提供同步保证。机器负载、调度策略和运行环境改变后线程顺序仍可能变化。12. 什么时候读写锁比互斥锁合适读写锁更适合共享数据读多写少一次读取工作并非极短多个读者并发确实能提高吞吐量能接受更复杂的锁规则和潜在调度问题。普通互斥锁可能更合适写操作频繁临界区很短并发读带来的收益很小更重视代码简单和容易验证。读写锁并不天然比互斥锁快应通过实际负载测量。13. 常见错误错误后果正确做法读者加锁写者不加锁同步协议失效仍有数据竞争所有读写者使用同一把锁写线程误用rdlock多个写者可能同时进入修改共享状态必须用wrlock读取可变数据时完全不加锁可能看到不一致状态获取读锁后再读取忘记unlock后续线程长期阻塞每条退出路径都释放锁未持锁却调用unlock未定义行为只释放当前线程实际持有的锁锁仍被持有时销毁未定义行为所有线程结束并释放后销毁用sleep规定执行顺序结果随调度变化使用正式同步机制认为先创建必然先执行业务顺序不可靠显式表达依赖关系在读锁中修改共享数据破坏多个读者并发的前提修改时获取写锁14. 录音与教材表述校正转写中的“图形锁”“图像处理”在本节多数应为“读写锁”。读者能够并发不表示读共享内存永远安全如果存在写者读者也必须参与同步。pthread_rwlock_t的内部结构属于具体实现应用代码不应依赖其字段。动态初始化必须传两个参数pthread_rwlock_init(rwlock, NULL)。静态初始化使用PTHREAD_RWLOCK_INITIALIZER不能再对同一对象重复初始化。读锁和写锁都通过同一个pthread_rwlock_unlock()释放。两个未加锁写线程得到 1 是教学中常见现象但数据竞争属于未定义行为不能保证每次都得到同样结果。创建写线程后调用sleep(3)只能增加随后读到最终值的概率不能构成严格同步。读写锁释放后究竟由读者还是写者先获得不能依赖普通程序中的固定猜测。15. 本节最低掌握标准学完后应能回答读锁和写锁分别允许哪些线程并发为什么读线程也要加锁为什么写线程必须使用pthread_rwlock_wrlock()静态初始化和动态初始化怎样写读锁和写锁分别怎样释放为什么pthread_create()的先后顺序不等于实际执行顺序sleep()为什么不能替代同步机制什么情况下读写锁可能比互斥锁更合适最低实践要求独立写出一个读线程和一个写线程正确使用rdlock → 读取 → unlock正确使用wrlock → 修改 → unlock等待所有线程结束后再销毁读写锁能解释未加写锁时发生的丢失更新。16. PDF 页码与录音时间索引主题PDF 页码录音时间读写锁工作原理15000:05—01:00类型、初始化与销毁15101:00—04:23rdlock、wrlock、unlock151—15202:00—04:23未加写锁的实验152—15404:24—11:45给写操作添加写锁154—15611:46—13:55读写操作顺序实验准备156—15813:56—15:02写饥饿158 页起本次截止点之后下一节学习进一步核对资料POSIX Programmer’s Manual读写锁初始化与销毁POSIX Programmer’s Manual获取读锁POSIX Programmer’s Manual获取写锁POSIX Programmer’s Manual释放读写锁17. 下一节预习边界第 158 页后半开始讨论写饥饿读者持续到来时等待中的写者可能长时间无法获得写锁。这个主题对应录音 17:56 以后不属于本次 15:02 前的学习范围。继续学习时重点关注为什么已有写者等待时新读者是否还能进入会影响公平性Linux 中读写锁属性怎样改变读写优先策略为什么“读者并发越多越好”并不总是成立。
返回列表