有些数据只需要在初始化时进行保护,由于是只读数据,后面也不进行更新。这时在初始化完成后,访问变量时仍然先去加锁,就多余和影响效率了。c++标准为这样的场景提供了一个机制:
有些开销大的对象需要延迟初始化,比如数据库连接或者分配大内存。在单线程中需要先判断是否为空,然后再去初始化。在多线程环境下,需要加锁访问,这会导致程序被串行执行,许多人使用
双重检查锁方法:即先判断指针是空的情况下,再去加锁和初始化。不幸的是会导致数据竞争。因为锁外面的读操作并没有和加锁后的写操作同步。即使另外的线程看到了对指针写操作,也不保证看到
了新初始化的对象,后面操作这个对象就可能会出现不正确的结果。这就是数据竞争,会导致未定义结果,无疑要避免这种行为。
标准提供了std::once_flag和std::call_once来处理这种场景。当std::call_once 返回时,可以确保有一个线程完成了指针的初始化。相比使用锁的方式,开销更小,特别时已经初始化以后。
所以应该尽可能使用这种方式。
另外一种避免初始化时的数据竞争是采用静态局部变量方式。静态局部变量的初始化发生在控制流首次遇到其定义时候。对于多线程调用,意味着可能存在并发数据竞争问题。对于c++11之前的版本来说,
实际上是有问题的,因为可能每个线程都认为自己是第一个,从而去初始化,也可能在别的线程正在初始化还没有完成时候去使用。在c++11后解决了这个问题,只允许一个线程去初始化,而这个过程中,别的线程
必须等待,所以全部的数据竞争就是哪个线程去实施初始化,不会再有数据并发问题了。
更一般的场景是保护一个不经常更新的对象。比如DNS数据不经常更新,但偶尔也会有更新,所以也要考虑数据竞争。可以使用读写锁,std::shared_mutex . 一旦线程以共享的方式获取到锁, 别的用独占
方式获取锁的线程就要阻塞,直到所有的共享锁线程都释放锁。同样一旦获取到独占锁,那么使用共享锁的线程也必须等待。