当我们想要将一些可以执行的代码放在后台,可以用线程将这段程序运行起来。然后,我们就等待运行的结果就好:
std::thread t {my_function, arg1, arg2, ...};
// do something else
t.join(); // wait for thread to finish
这里t.join()
并不会给我们my_function
函数的返回值。为了获取返回值,需要先实现my_function
函数,然后将其返回值存储到主线程能访问到的地方。如果这样的情况经常发生,我们就要重复的写很多代码。
C++11之后,std::async
能帮我们完成这项任务。我们将写一个简单的程序,并使用异步函数,让线程在同一时间内做很多事情。std::async
其实很强大,让我们先来了解其一方面。
我们将在一个程序中并发进行多个不同事情,不显式创建线程,这次使用std::async
和std::future
:
-
包含必要的头文件,并声明所使用的命名空间:
#include <iostream> #include <iomanip> #include <map> #include <string> #include <algorithm> #include <iterator> #include <future> using namespace std;
-
实现了三个函数,算是完成些很有趣的任务。第一个函数能够接收一个字符串,并且创建一个对于字符串中的字符进行统计的直方图:
static map<char, size_t> histogram(const string &s) { map<char, size_t> m; for (char c : s) { m[c] += 1; } return m; }
-
第二个函数也能接收一个字符串,并返回一个排序后的副本:
static string sorted(string s) { sort(begin(s), end(s)); return s; }
-
第三个函数会对传入的字符串中元音字母进行计数:
static bool is_vowel(char c) { char vowels[] {"aeiou"}; return end(vowels) != find(begin(vowels), end(vowels), c); } static size_t vowels(const string &s) { return count_if(begin(s), end(s), is_vowel); }
-
主函数中,我们从标准输入中获取字符串。为了不让输入字符串分段,我们禁用了
ios::skipws
。这样就能得到一个很长的字符串,并且不管这个字符串中有多少个空格。我们会对结果字符串使用pop_back
,因为这种方式会让一个字符串中包含太多的终止符:int main() { cin.unsetf(ios::skipws); string input {istream_iterator<char>{cin}, {}}; input.pop_back();
-
为了获取函数的返回值,并加快对输入字符串的处理速度,我们使用了异步的方式。
std::async
函数能够接收一个策略和一个函数,以及函数对应的参数。我们对于这个三个函数均使用launch::async
策略。并且,三个函数的输入参数是完全相同的:auto hist (async(launch::async, histogram, input)); auto sorted_str (async(launch::async, sorted, input)); auto vowel_count (async(launch::async, vowels, input));
-
async
的调用会立即返回,因为其并没有执行我们的函数。另外,准备好同步的结构体,用来获取函数所返回的结果。目前的结果使用不同的线程并发的进行计算。此时,我们可以做其他事情,之后再来获取函数的返回值。hist
,sorted_str
和vowel_count
分别为函数histogram
,sorted
和vowels
的返回值,不过其会通过std::async
包装入future
类型中。这个对象表示在未来某个时间点上,对象将会获取返回值。通过对future
对象使用.get()
,我们将会阻塞主函数,直到相应的值返回,然后再进行打印:for (const auto &[c, count] : hist.get()) { cout << c << ": " << count << '\n'; } cout << "Sorted string: " << quoted(sorted_str.get()) << '\n' << "Total vowels: " << vowel_count.get() << '\n'; }
-
编译并运行代码,就能得到如下的输出。我们使用一个简短的字符串的例子时,代码并不是真正的在并行,但这个例子中,我们能确保代码是并发的。另外,程序的结构与串行版本相比,并没有改变多少:
$ echo "foo bar baz foobazinga" | ./async : 3 a: 4 b: 3 f: 2 g: 1 i: 1 n: 1 o: 4 r: 1 z: 2 Sorted string: " aaaabbbffginoooorzz" Total vowels: 9
如果你没有使用过std::async
,那么代码可以简单的写成串行代码:
auto hist (histogram(input));
auto sorted_str (sorted( input));
auto vowel_count (vowels( input));
for (const auto &[c, count] : hist) {
cout << c << ": " << count << '\n';
}
cout << "Sorted string: " << quoted(sorted_str) << '\n';
cout << "Total vowels: " << vowel_count << '\n';
下面的代码,则是并行的版本。我们将三个函数使用async(launch::async, ...)
进行包装。这样三个函数都不会由主函数来完成。此外,async
会启动新线程,并让线程并发的完成这几个函数。这样我们只需要启动一个线程的开销,就能将对应的工作放在后台进行,而后可以继续执行其他代码:
auto hist (async(launch::async, histogram, input));
auto sorted_str (async(launch::async, sorted, input));
auto vowel_count (async(launch::async, vowels, input));
for (const auto &[c, count] : hist.get()) {
cout << c << ": " << count << '\n';
}
cout << "Sorted string: "
<< quoted(sorted_str.get()) << '\n'
<< "Total vowels: "
<< vowel_count.get() << '\n';
例如histogram
函数则会返回一个map
实例,async(..., histogram, ...)
将返回给我们的map
实例包装进之前就准备好的future
对象中。future
对象时一种空的占位符,直到线程执行完函数返回时,才有具体的值。结果map
将会返回到future
对象中,所以我们可以对对象进行访问。get
函数能让我们得到被包装起来的结果。
让我们来看一个更加简单的例子。看一下下面的代码:
auto x (f(1, 2, 3));
cout << x;
与之前的代码相比,我们也可以以下面的方式完成代码:
auto x (async(launch::async, f, 1, 2, 3));
cout << x.get();
这都是最基本的。后台执行的方式可能要比标准C++出现还要早。当然,还有一个问题要解决:launch::async
是什么东西? launch::async
是一个用来定义执行策略的标识。其有两种独立方式和一种组合方式:
策略选择 | 意义 |
---|---|
launch::async | 运行新线程,以异步执行任务 |
launch::deferred | 在调用线程上执行任务(惰性求值)。在对future 调用get 和wait 的时候,才进行执行。如果什么都没有发生,那么执行函数就没有运行。 |
launch::async | launch::deferred | 具有两种策略共同的特性,STL的async 实现可以的选择策略。当没有提供策略时,这种策略就作为默认的选择。 |
Note:
不使用策略参数调用
async(f, 1, 2, 3)
,我们将会选择都是用的策略。async
的实现可以自由的选择策略。这也就意味着,我们不能确定任务会执行在一个新的线程上,还是执行在当前线程上。
还有件事情我们必须要知道,假设我们写了如下的代码:
async(launch::async, f);
async(launch::async, g);
这就会让f
和g
函数并发执行(这个例子中,我们并不关心其返回值)。运行这段代码时,代码会阻塞在这两个调用上,这并不是我们想看到的情况。
所以,为什么会阻塞呢?async
不是非阻塞式、异步的调用吗?没错,不过这里有点特殊:当对一个async
使用launch::async
策略时,获取一个future
对象,之后其析构函数将会以阻塞式等待方式运行。
这也就意味着,这两次调用阻塞的原因就是,future
生命周期只有一行的时间!我们可以以获取其返回值的方式,来避免这个问题,从而让future
对象的生命周期更长。