飞道的博客

C++(标准库):49---并发之(一次调用:std::once_flag、std::call_once)

473人阅读  评论(0)

一、先来看一些例子

  • 有些代码中,某些代码会被多线程使用,但是当一个线程使用之后,其他线程就不能再去使用了
  • 例如:
    • 下面的代码会调用一个initialize()初始化函数对某些东西进行初始化,当一个线程去初始化之后,另外的线程再次执行时就不需要再次去初始化了
    • 但是在多线程环境下,下面的if会造成data race,因为多个线程可能同时执行到了if,并且没有任何措施,导致都执行了if,因此造成两次初始化,与我们的初衷不符合

  
  1. void initialize();
  2. bool initialized = false;
  3. //多线程环境操作下,此处会发生data race
  4. if (!initialized)
  5. {
  6. initialize();
  7. initialized = true;
  8. }
  • 例如:
    • ​​​​​​​​​​​​​​下面的代码与上面的演示案例类似。在多线程环境下时,可能有多个线程执行到foo()函数中的if,造成data race

  
  1. static std:: vector< std:: string> staticData;
  2. std:: vector< std:: string> initializeStaticData();
  3. void foo()
  4. {
  5. //多线程环境操作下,此处会发生data race
  6. if (staticData.empty())
  7. {
  8. staticData = initializeStaticData();
  9. }
  10. }
  • 这些案例在单线程中不会发生错误,但是在多线程环境中,如果多个线程对数据进行访问,那么就会发生data race

二、std::once_flag、std::call_once

  • 对于上面的data race,有两种解决方案:
    • 一种使用mutex来对共享数据进行保护操作,确保在同一时间下,只有一个线程可以对共享数据尽心操作
    • C++标准库还提供了一个特殊的解法,那就是使用std::once_flag和std::call_once(头文件为<mutex>)
  • 例如在文章最开始的两个data race例子,我们可以将代码修改为下:

  
  1. void initialize();
  2. void foo()
  3. {
  4. std::once_flag oc;
  5. //...
  6. //执行initialize函数,当下次再次执行到这个语句时,就不会再去执行了,因为oc被执行过一次了
  7. std::call_once(oc, initialize);
  8. }

  
  1. static std:: vector< std:: string> staticData;
  2. std:: vector< std:: string> initializeStaticData();
  3. void foo()
  4. {
  5. static std::once_flag oc;
  6. //执行initializeStaticData函数,当下次再次执行到这个语句时,就不会再去执行了,因为oc被执行过一次了
  7. std::call_once(oc, [] {staticData = initializeStaticData(); });
  8. }
  • std::call_once的参数:
    • 参数1为一个std::once_flag变量
    • 参数2为一个可调用对象,如function、member function或lambda
    • 后面的参数是可选的,作为可调用对象的参数传递

工作原理

  • 对于同一段代码,当call_once()被调用一次之后,当再次执行到这段代码时,once_flag标志被执行过一次之后,就不会再次执行了,因此达到了call_once中的可调用对象只能执行一次的目的

三、典型使用案例

  • 典型的例子就是lazy initialization(缓式初始化):第一次某个线程需要某数据而该数据必须备妥,于是此时处理它(但不在先前处理,因为你想节省空间;如果非必要的话就不处理它)
  • 因此,多线程环境下的缓式初始化应该如下所示:

  
  1. class X
  2. {
  3. public:
  4. data getData()const
  5. {
  6. std::call_once(initDataFlag, &X::initData, this);
  7. //...
  8. }
  9. private:
  10. mutable std::once_flag initDataFlag;
  11. void initData()const;
  12. };

四、相关注意事项

  • 原则上你可以使用同一个once_flag调用不同的函数。之所以把once_flag当做第一实参传递给call_once()就是为了确保传入的机能只能被执行一次。因此,如果第一次调用成功,下一次调用又带着相同的once_flag,传入的技能就不会被调用——即使该机能与第一次有异
  • 被调用的函数所造成的任何异常会被call_once()抛出。此情况第一次调用被视为不成功,因此下一次call_once()还可以再调用它所接受的技能

转载:https://blog.csdn.net/qq_41453285/article/details/105604024
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场