单写多读环境下,写线程的my_read_1是否需要加读锁?
单写多读场景下写线程读取共享变量是否需要加锁?
我知道多线程读写共享变量时必须加锁避免并发问题,但在单写多读(single writer multiple reader)环境里,唯一的写线程自己读取这个变量时,要不要加读锁?我觉得不用,因为只有它自己会修改,不会有其他线程改,应该不会出现未定义行为。换句话说,代码里的my_read_1到底需不需要加读锁?省略的话会有什么后果?
/********* Thread A *********/ hlist_head_t g_my_table[MY_TABLE_SIZE]; pthread_rwlock_t g_my_lock; void my_update(uint64_t hash, hlist_node_t *node) { pthread_rwlock_wrlock(&g_my_lock); hlist_head_t *head = &g_my_table[hash % MY_TABLE_SIZE]; hlist_add_head(node, head); pthread_rwlock_unlock(&g_my_lock); } void my_read_1(uint64_t hash) { // TODO: is read lock necessary here? hlist_head_t *head = &g_my_table[hash % MY_TABLE_SIZE]; if (hlist_empty(head)) { printf("empty\n"); } else { printf("%p %p\n", head->first->next, head->first->pprev); } } /********* Thread B *********/ extern hlist_head_t g_my_table[MY_TABLE_SIZE]; extern pthread_rwlock_t g_my_lock; void my_read_2(uint64_t hash) { pthread_rwlock_rdlock(&g_my_lock); hlist_head_t *head = &g_my_table[hash % MY_TABLE_SIZE]; if (hlist_empty(head)) { printf("empty\n"); } else { printf("%p %p\n", head->first->next, head->first->pprev); } pthread_rwlock_unlock(&g_my_lock); }
结论:my_read_1必须加读锁,省略锁会引发多个严重问题
为什么必须加锁?
- 避免读取中间状态:写线程执行
my_update时,hlist_add_head是一个非原子的链表修改操作(需要修改多个指针)。如果此时写线程同时调用my_read_1(没加锁),会读取到链表的半修改状态,比如hlist_empty返回false,但head->first的指针还没完全初始化,导致printf时访问非法内存或者输出错误值。 - 保证内存可见性:现代CPU的缓存机制和编译器优化可能导致写线程完成的修改没有同步到主存,
my_read_1不加锁的话,可能直接从寄存器或缓存读取旧值,出现逻辑错误。而读写锁的加解锁操作会触发内存屏障,强制同步缓存与主存,确保读取到最新数据。 - 避免死锁与逻辑冲突:pthread_rwlock默认不支持重入,如果写线程在
my_read_1(无锁)执行过程中调用my_update,my_update会尝试获取写锁——此时如果有其他读线程持有读锁,写线程会被阻塞,而其他读线程可能等待写锁释放(如果后续有写操作需求),最终导致死锁。 - 代码一致性与可维护性:统一对共享变量的所有访问加锁,能避免后续维护时误将
my_read_1复用在其他线程,或者修改逻辑时破坏并发安全,减少出错概率。
省略读锁的直接后果
- 程序崩溃或数据错误:读取到链表的中间状态,导致空指针访问、非法内存引用,或者输出错误的指针值。
- 逻辑异常:由于内存可见性问题,写线程读取到旧的链表数据,触发错误的分支判断(比如明明刚添加了节点,却判断链表为空)。
- 死锁风险:在多线程交互场景下,可能引发写线程与读线程的互相等待,导致程序卡死。
- 破坏并发控制语义:读写锁的排他性与一致性保障被打破,整个共享资源的访问逻辑变得不可靠。
内容的提问来源于stack exchange,提问作者davidhcefx
相关产品推荐
相关产品推荐

