11.1 与并发相关的错误类型

    • 条件竞争

    这两大类的颗粒度很大,让我们将其分成颗粒度较小的问题。

    “不必要的阻塞”是什么意思?一个线程阻塞的时候,不能处理任何任务,因为它在等待其他“条件”的达成。通常这些“条件”就是一个互斥量、一个条件变量或一个期望值,也可能是一个I/O操作。这是多线程代码的先天特性,不过这也不是在任何时候都可取的——衍生成“不必要的阻塞”。你会问:为什么不需要阻塞?通常,因为其他线程在等待该阻塞线程上的某些操作完成,如果该线程阻塞了,那些线程必然会被阻塞。

    • 死锁——如在第3章所见,死锁的情况下,两个线程会互相等待。当线程产生死锁,应该完成的任务就会持续搁置。举个例子来说,一些线程是负责对用户界面操作的线程,在死锁的情况下,用户界面就会无响应。另一些例子中,界面接口会保持响应,不过有些任务就无法完成,比如:查询无结果返回或文档未打印。

    • I/O阻塞或外部输入——当线程被外部输入所阻塞,线程也就不能做其他事情了(即使,等待输入的情况永远不会发生)。因此,被外部输入所阻塞,就会让人不太高兴,因为可能有其他线程正在等待这个线程完成某些任务。

    简单的介绍了一下“不必要阻塞”的组成。那么,条件竞争呢?

    11.1.2 条件竞争

    特别是,条件竞争经常会产生以下几种类型的错误:

    • 数据竞争——因为未同步访问一块共享内存,将会导致代码产生未定义行为。第5章已经介绍了数据竞争,也了解了C++的内存模型。数据竞争通常发生在错误的使用原子操作上,做同步线程的时候,或没使用互斥量保护共享数据的时候。

    • 生命周期问题——虽然这类问题也能归结为破坏了不变量,不过这里将其作为一个单独的类别给出。这里的问题是线程会访问不存在的数据,这可能是因为数据被删除或销毁了,或者转移到其他对象中去了。生命周期问题,通常是在一个线程引用了局部变量,在线程还没有完成前,局部变量的“死期”就已经到了,不过这个问题并不止存在这种情况下。当手动调用join()等待线程完成工作,需要保证异常抛出的时候,join()还会等待其他未完成工作的线程。这是线程中基本异常安全的应用。

    现在已经了解了这两大类中都有哪些具体问题了。下面就让我们来了解,如何在代码中定位和修复这些问题。