首先,引入一个概念叫做reorder,即重新安排。编译器编译出来的代码是一个黑盒子,标准对此的要求是,只要程序的行为在外观上相同即可。因此语句的reorder可能在单线程环境下并无问题,但是多线程组合以后,往往会改变预期的行为。
其次,单线程环境下,用条件判断获得的保证,在多线程中是失效的。比如if (container.empty() != true) doSomethingInContainer,这样的代码可能会在多线程环境中抛出异常,因为在通过if判断之后,对容器执行操作之前,可能会有其他线程修改容器,导致本来不为空(刚通过if判断时)的容器变成空的。正式因此这样的问题,才有了后来所谓的“double check”,既保证效率,又可以避免同时访问/修改数据。但是即使是double check也存在一些问题,比如上面的reorder就可能会破坏它的行为。
最后,线程可能访问的是写到半途的数据。比如有全局变量uint32_t value; 某线程执行value = 0; 而另外一个线程读取value的值,那么可能读取的是写到一半(两个字节),还有另外一半未被写入的数据。所以,即使是基础类型,也不保证读或写是atomic。
解决这些问题的基本方法包括互斥锁和条件变量。
互斥锁使用起来比较简单,值得注意是,当同时使用多个锁时,加锁的顺序要一致,否则会出现死锁。有时保证一致的加锁顺序并不轻而易举。例如在一个满足交换律的操作中,对两者同时加锁。这时需要先确定对象身份,然后再按照对象1、对象2的顺序加锁。在C++中可以使用lock()函数对多个锁进行加锁。另外,在C++中应该使用RAII管理锁资源,保证在异常发生时依然可以释放锁。
条件变量经常用于同步多个线程,保证数据流的逻辑正确。值得注意的是,在C++中条件变量可能会发生假醒,所以在唤醒之后,还需要检查所需数据是否已经真正达到所需状态。对条件变量的wait()操作必须使用互斥锁保护,而唤醒操作不需要。
通常会使用lock_guard来管理锁,初始化时锁定,销毁时解锁。也可以使用unique_lock来管理锁,unique_lock可以控制锁定和解锁,销毁时判断拥有的锁是否锁定,如果是,则解锁。条件变量condition_variable因为会在中途解锁上锁,所以需要配合unique_lock而不能使用lock_guard。