有时我们有一些特定的代码段,可以在多个线程间并行执行,不过其中有一些功能需要在进行执行前,完成一次初始化的过程。一个很简单的方式,就是在程序进入并行前,执行已存在的准备函数。
这种方法有如下几个缺点:
- 当并行线程来自于一个库,使用者肯定会忘记调用准备函数。这样会让库函数不是那么容易的让人使用。
- 当准备函数特别复杂,并且在某些条件下我们要通过条件来判断,是否要执行这个准备函数。
本节中,我们将来了解一下std::call_once
,其能帮助使用简单且优雅的方式解决上面提到的问题。
我们将完成一个程序,我们使用多线程对同一段代码进行执行。虽然这里执行的是相同的代码,但是我们的准备函数只需要运行一次:
-
包含必要的头文件,并声明所使用的命名空间:
#include <iostream> #include <thread> #include <mutex> #include <vector> using namespace std;
-
我们将使用
std::call_once
。为了对其进行使用,需要对once_flag
进行实例化。在对指定函数使用call_once
时,需要对所有线程进行同步:once_flag callflag;
-
现在来定义一个只需要执行一次的函数,就让这个函数打印一个感叹号吧:
static void once_print() { cout << '!'; }
-
再来定义所有线程都会运行的函数。首先,要通过
std::call_once
调用once_print
。call_once
需要我们之前定义的变量callflag
。其会被用来对线程进行安排:static void print(size_t x) { std::call_once(callflag, once_print); cout << x; }
-
OK,让我们启动10个线程,并且让他们使用
print
函数进行执行:int main() { vector<thread> v; for (size_t i {0}; i < 10; ++i) { v.emplace_back(print, i); } for (auto &t : v) { t.join(); } cout << '\n'; }
-
编译并运行程序,我们就会得到如下的输出。首先,我们可以看到由
once_print
函数打印出的感叹号。然后,我么可以看到线程对应的ID号。另外,其会对所有线程进行同步,所以不会有ID在once_print
函数执行前被打印:$ ./call_once !1239406758
std::call_once
工作原理和栅栏类似。其能对一个函数(或是一个可调用的对象)进行访问。第一个线程达到call_once
的线程会执行对应的函数。直到函数执行结束,其他线程才能不被call_once
所阻塞。当第一个线程从准备函数中返回后,其他线程也就都结束了阻塞。
我们可以对这个过程进行安排,当有一个变量决定其他线程的运行时,线程则必须对这个变量进行等待,直到这个变量准备好了,所有变量才能运行。这个变量就是once_flag callflag;
。每一个call_once
都需要一个once_flag
实例作为参数,来表明预处理函数是否运行了一次。
另一个细节是:如果call_once
执行失败了(因为准备函数抛出了异常),那么下一个线程则会再去尝试执行(这种情况发生在下一次执行不抛出异常的时候)。