C++ 标准库 thread_thread命名空间以及std::once_flag、std::call_once

原文链接:并发之(thread_thread命名空间) 并发之(一次调用:std::once_flag、std::call_once)

thread_thread命名空间

  • 针对任何线程(包括主线程),声明了一个命名空间std::this_thread,用以提供线程专属的global函数
  • 支持的操作如下:

yield()

  • 函数this_thread::yield()用来告诉系统,放弃当前线程的时间切片余额是有好处的,这将使运行环境得以重新调度以便允许其他线程执行
  • 放弃控制”的一个典型例子是当等待或轮询另一线程,或等待或轮询“某个atomic flag被另一线程设定”:

  • 另一个例子是,当你尝试锁定多个lock/mutex却无法取得其中一个lock或mutex,那么在尝试不同次序的lock/mutex之前你可以使用yield(),这会让你的程序更快些

一次调用:std::once_flag、std::call_once

  • 先来看一些例子
  • 有些代码中,某些代码会被多线程使用,但是当一个线程使用之后,其他线程就不能再去使用了
  • 例如:
    • 下面的代码会调用一个initialize()初始化函数对某些东西进行初始化,当一个线程去初始化之后,另外的线程再次执行时就不需要再次去初始化了
    • 但是在多线程环境下,下面的if会造成data race,因为多个线程可能同时执行到了if,并且没有任何措施,导致都执行了if,因此造成两次初始化,与我们的初衷不符合
void initialize();
 
bool initialized = false;
//多线程环境操作下,此处会发生data race
if (!initialized)
{
    initialize();
    initialized = true;
}
  • 例如:
    • 下面的代码与上面的演示案例类似。在多线程环境下时,可能有多个线程执行到foo()函数中的if,造成data race
static std::vector staticData;
std::vector initializeStaticData();
 
void foo()
{
    //多线程环境操作下,此处会发生data race
    if (staticData.empty())
    {
        staticData = initializeStaticData();
    }
}
  • 这些案例在单线程中不会发生错误,但是在多线程环境中,如果多个线程对数据进行访问,那么就会发生data race

std::once_flag、std::call_once

  • 对于上面的data race,有两种解决方案:
    • 一种使用mutex来对共享数据进行保护操作,确保在同一时间下,只有一个线程可以对共享数据尽心操作
    • C++标准库还提供了一个特殊的解法,那就是使用std::once_flag和std::call_once(头文件为
  • 例如在文章最开始的两个data race例子,我们可以将代码修改为下:
void initialize();
 
void foo()
{
    std::once_flag oc;
    //...
 
    //执行initialize函数,当下次再次执行到这个语句时,就不会再去执行了,因为oc被执行过一次了
    std::call_once(oc, initialize);
}
static std::vector staticData;
std::vector initializeStaticData();
 
void foo()
{
    static std::once_flag oc;
    //执行initializeStaticData函数,当下次再次执行到这个语句时,就不会再去执行了,因为oc被执行过一次了
    std::call_once(oc, [] {staticData = initializeStaticData(); });
}
  • std::call_once的参数:
    • 参数1为一个std::once_flag变量
    • 参数2为一个可调用对象,如function、member function或lambda
    • 后面的参数是可选的,作为可调用对象的参数传递

工作原理

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

典型使用案例

  • 典型的例子就是lazy initialization(缓式初始化):第一次某个线程需要某数据而该数据必须备妥,于是此时处理它(但不在先前处理,因为你想节省空间;如果非必要的话就不处理它)
  • 因此,多线程环境下的缓式初始化应该如下所示:
class X
{
public:
    data getData()const
    {
        std::call_once(initDataFlag, &X::initData, this);
        //...
    }
private:
    mutable std::once_flag initDataFlag;
    void initData()const;
};

相关注意事项

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

你可能感兴趣的:(#,C++标准库)