4.3线程安全接口(Thread-Safe Interface)
1.问题
多线程组件通常包括多个可被公共访问的接口方法以及可以改变组件状态的私有方法。为了避免出现竞争条件,可以使用一个组件内部的锁对访问其状态的接口方法调用串行化。尽管当每个方法都自包容的时候,这种设计方法能很好地工作。但组件方法可能会相互调用完成计算任务。如果是这样,在多线程组件中有以下几个强制条件尚未解决,它们使用了错误的组件间方法调用的设计方法:
1)应该使线程安全组件避免“自死锁”。当一个组件方法在该组件中获得一个非递归锁,然后调用试图获得同一个锁的另一个组件方法时,会发生自死锁。
2)为了避免出现对组件状态的竞争条件等情况,应该使线程安全组件具有最小的加锁开销。如果为避免上面介绍的自死锁问题而选择一个递归组件锁,由于组件间方法调用多次获得和释放锁,而会产生不必要的开销。
2.解决方案
按照以下两个约定将处理组件间方法调用的所有组件初始化:
1)接口方法检查。所有接口方法(如C++公有方法)应该只获得/释放组件锁,在组件的“边缘”进行同步检查。获得一个锁后,接口方法立即转发给一个实现方法,后者执行实际的方法功能。实现方法返回后,接口在将控制返回给调用者之前释放该锁。
2)实现方法信任。只有被接口方法调用,实现方法(如C++的私有或保护方法)才能完成它们的任务。因此它们相信只有在获得锁后才会调用它们,它们本身不应该获得或释放锁。实现方法也不应该“向上”调用接口方法,因为接口要获得锁。
3.实现
1)确定接口和相应的实现方法。接口方法定义组件的公共API,为每个接口方法对应地定义一个实现方法。
2)对接口和实现方法进行编程。
4.结论
优点:
1)提高了健壮性。由于使用组件间方法调用,该模式可以避免自死锁。
2)改善了性能。该模式确保在必要时才获得或释放锁。
3)简化了软件。将加锁和功能特性问题分开,可以使加锁和功能特性得到简化。
不足:
1)增加间接的和额外的方法。
2)潜在的死锁。
3)潜在的误用。
4)潜在的开销。
4.4双检查加锁优化(double-checked locking optimization)
1.问题
串行化加锁方法对于只要求执行一次初始化的对象或组件是不合适的,例如Singleton中的临界区代码在其初始化阶段必须执行一次。但是对Singleton的每个方法调用都要获取和释放互斥锁,从而过多地增加了开销,为避免这些开销,并发应用的程序员可能转而使用全局变量而不是单件模式。不过这种解决方案有两个缺点:
·可移植性不好。因为通常没有指定在不同文件中定义的全局对象被构造的顺序。
·资源的浪费。因为即使不使用全局变量,也要产生全局变量。
2.解决方案
引入一个标志,表示在获得一个保证临界区的锁之前确定是否需要执行该临界区。如果不需要执行该代码,就跳过该临界区,从而避免了不必要的加锁开锁。在临界区内增加相同的标志检测,表示进入临界区后是否需要初始化资源,从而避免了资源被多次初始化。
3.实现
1)确定要仅执行一次的临界区。该临界区进行的操作(如初始化逻辑)在程序中仅执行一次。
2)实现加锁逻辑。加锁逻辑对只执行一次的临界区代码的访问串行化。
3)实现首次进入(first-time-in)标志。该标志表示临界区是否被执行过。
4.结论
优点:
1)使加锁开销最小。
2)防止竞争条件。
不足:
1)非原子指针或集成赋值语义。
2)多处理器缓存的连贯性。
3)额外的互斥使用。