C++并发编程(九)
前言
我们设计程序时,有时会出现这种状况,一条主线,一条支线,两条线分别做不同的事情,最后合并为一支。
我们可以用thread开启线程,但如果需要回传结果,稍显麻烦,此时可以用 std::async() 函数和 std::future 对象解决。
分离线程功能,还可用任务包 std::packaged_task 在线程间传递任务,由多个线程发起传递任务,由固定功能线程接收线程包,执行任务。
一、从后台任务返回值
通过 std::async() 函数将与主线无关的部分分离,在恰当的时间返回结果,很容易实现程序的异步多支线逻辑。
#include <future>
#include <iostream>
auto findTheAnswerToLtuae(int i) -> int
{
return i * i;
}
void doOtherStuff()
{
std::cout << "do something." << std::endl;
}
auto main() -> int
{
std::future<int> theAnswer = std::async(findTheAnswerToLtuae, 5);
doOtherStuff();
std::cout << "the answer is " << theAnswer.get() << std::endl;
return 0;
}
std::async() 函数的调用方法,可以传入普通的函数指针,及后续实参,如果是类的成员函数,可以传入成员函数指针,类对象指针(或类对象本身,或std::ref()包装的对象),及后续实参。
#include <future>
#include <iostream>
#include <string>
struct X
{
void foo(int i, const std::string &str)
{
std::cout << i << str << std::endl;
}
auto bar(const std::string &str) -> std::string
{
std::cout << str << std::endl;
return str;
}
};
struct Y
{
auto operator()(double d) -> double
{
std::cout << d << std::endl;
return d;
}
};
struct moveOnly
{
moveOnly() = default;
moveOnly(moveOnly && /*unused*/) noexcept
{}
moveOnly(const moveOnly &) = delete;
auto operator=(moveOnly && /*unused*/) noexcept -> moveOnly &
{
return *this;
}
auto operator=(const moveOnly &) -> moveOnly & = delete;
void operator()()
{
std::cout << "moveOnly." << std::endl;
}
};
auto baz(X &x) -> X
{
return x;
}
auto main() -> int
{
X x;
//调用成员函数和 std::thread 实例的调用方法相同
//传入成员函数指针,对象指针,函数实参
auto f1 = std::async(&X::foo, &x, 42, "hello");
//直接传入对象,则会构造对象副本,不影响原对象
auto f2 = std::async(&X::bar, x, "goodbye");
Y y;
//调用仿函数
auto f3 = std::async(Y(), 3.14);
//通过传入可执行对象的引用调用仿函数
auto f4 = std::async(std::ref(y), 2.718);
//调用普通函数,引用类型的参数需std::ref()函数进行传入
auto f5 = std::async(baz, std::ref(x));
//调用仿函数
auto f6 = std::async(moveOnly());
//强制新线程调用仿函数
auto f7 = std::async(std::launch::async, Y(), 1.2);
//强制本线程递延调用函数指针
auto f8 = std::async(std::launch::deferred, baz, std::ref(x));
//由编译器自行选择是否开启新线程或本线程递延调用
auto f9 = std::async(std::launch::deferred | std::launch::async, baz,
std::ref(x));
//此时开始执行
f8.wait();
return 0;
}
二、线程间传递任务
多线程程序设计中,我们可以将某些线程设计为专注于某些功能,其他线程通过消息传递给这些功能线程,完成特定功能。
例如GUI框架,由专门的界面更新线程,负责界面更新,用 std::packaged_task 对象实现。
通过线程任务发送函数 postTaskForGuiThread,将预执行任务加入队列 tasksQue,界面更新线程函数 guiThread 轮询队列tasksQue,如果有任务,则执行。
像这种分离线程功能的设计,用 std::packaged_task 配合队列,可较为简单的得以实现。
#include <deque>
#include <future>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <utility>
#include <vector>
std::mutex m;
bool shutDown;
//任务队列
std::deque<std::packaged_task<void()>> tasksQue;
auto guiShutdownMessageReceived() -> bool
{
return shutDown;
}
void getAndProcessGuiMessage()
{
std::cout << "process gui message!" << std::endl;
}
// gui运行线程
void guiThread()
{
//判断是否程序结束
while (!guiShutdownMessageReceived())
{
//取得gui信息,执行
getAndProcessGuiMessage();
//任务包
std::packaged_task<void()> task;
{
std::lock_guard<std::mutex> lk(m);
//任务队列为空则继续轮询
if (tasksQue.empty())
{
continue;
}
//取得任务
task = std::move(tasksQue.front());
tasksQue.pop_front();
}
//运行任务
task();
}
}
// gui后台线程
std::thread guiBgThread(guiThread);
int cnt = 0;
//测试
void test()
{
std::lock_guard<std::mutex> lk(m);
std::cout << "test" << cnt++ << std::endl;
if (cnt == 3)
{
shutDown = true;
}
}
//发送gui线程任务
template <typename Func>
auto postTaskForGuiThread(Func f) -> std::future<void>
{
//打包函数到任务
std::packaged_task<void()> task(f);
//取得任务future
std::future<void> resultFuture = task.get_future();
std::lock_guard<std::mutex> lk(m);
//将任务传递给任务队列
tasksQue.push_back(std::move(task));
//返回任务future
return resultFuture;
}
auto main() -> int
{
std::thread test1(postTaskForGuiThread<void()>, test);
std::thread test2(postTaskForGuiThread<void()>, test);
std::thread test3(postTaskForGuiThread<void()>, test);
test1.join();
test2.join();
test3.join();
guiBgThread.join();
return 0;
}
总结
当我们需要分离线程功能,可以通过标准库 std::async() 函数实现,也可通过多个线程进行任务包装 std::packaged_task, 用固定线程接收任务包,执行任务,实现稍复杂的线程功能分离逻辑。
参考文献:C++并发编程实战(第2版)[英] 安东尼•威廉姆斯(Anthony Williams)