线程间共享数据——共享数据导致的问题

一、前言

并发编程中的大多数数据问题都起因于共享数据被多个线程修改,读取只读属性的共享数据不会存在任何问题。

二、条件竞争

1. 条件竞争成因

在并发编程中,每一个线程都抢着完成自己的任务,但相对执行顺序却各不相同,这导致了条件竞争的产生。大多数情况下条件竞争是良性竞争,即线程执行的相对顺序不影响最终运行结果,如两个线程同时向一个容器中添加元素。自然地,也存在恶性竞争,例如在链表删除节点时另一个线程试图访问该节点。本书无意于关注与讨论良性竞争,提及的所有条件竞争均为恶性竞争。

恶性条件竞争通常发生在对多个数据块的修改操作中,但由于其出现概率较低,由此引发的问题很难查找与复现,仅有在系统负载增大时(执行线程数量增加),产生恶性竞争的执行序列复现概率才有所增大。此外,条件竞争通常是时间敏感的,因此在调试状态下很难定位到该bug,因为调试状态对执行时间存在一定的影响(虽然该影响并不甚大)。

三、避免恶性条件竞争

避免恶性条件竞争的方案有很多

1. 对数据结构采用某种保护机制

最直接的方案是对数据结构采用某种保护机制,从而保证仅有正在执行修改操作的线程才能够访问到结构正在被破坏时的中间状态。从其他执行线程的角度而言,修改操作不是还没执行,就是已经完成

2. 无锁编程

修改数据结构,从而保证其必然能够完成一系列不可分割的修改,这也就是所谓的”无锁编程”。该解决方案十分复杂,你必须深入了解内存模型的差异性与线程对数据的可访问性。

3. 软件事务内存

使用事务的方式去处理数据结构的更新(类似于数据库).所需的数据和读取都存储在事务日志中,然后将之前的操作合为一步,再进行提交。当数据结构被另一个线程修改后,或处于已经重启的情况下,提交将无法进行。C++并不直接支持STM,但相关思想将会在本书中有所说明。