并发编程 Vs. 异步编程
特性 | 并发编程 | 异步编程 |
---|---|---|
核心概念 | 多线程、多进程 | 非阻塞操作,常使用回调、Promises/Futures |
主要应用场景 | 提高CPU的利用效率,适用于计算密集型任务 | 提高I/O效率,保持程序响应性,适用于I/O密集型任务 |
资源管理 | 需要手动管理线程间的资源共享与数据一致性 | 系统自动处理任务调度和资源分配,有效减少复杂性 |
主要工具 | std::thread , std::mutex , std::lock , std::condition_variable | std::future , std::async , std::promise , 回调函数 |
设计和编写难度 | 复杂,尤其在管理和调试多线程同步和资源共享时 | 相对简单,通过非阻塞操作和事件驱动模型处理复杂任务 |
std::future
概念定义
std::thread
是C++11中提供异步创建多线程的工具,只能是异步运行任务,却无法获取任务执行的结果,一般都是依靠全局对象,全局对象在多线程下是及其不安全的,为此标准库提供了std::future
类模板来关联线程运行的函数和函数的返回结果,这种获取结果的方式是异步的。
std::future 通常由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。
一个有效的 std::future 对象通常由以下三种 Provider 创建,并和某个共享状态相关联。Provider 可以是函数或者类,他们分别是:
- std::async 函数
- std::promise::get_future,get_future 为 promise 类的成员函数
- std::packaged_task::get_future,此时 get_future为 packaged_task 的成员函数
一个 std::future 对象只有在有效(valid)的情况下才有用(useful),由 std::future 默认构造函数创建的 future 对象不是有效的(除非当前非有效的 future 对象被 move 赋值另一个有效的 future 对象)。
std::future成员函数
template<typename ResultType>
class future
{
public:
future() noexcept;
future(future&&) noexcept;
future& operator=(future&&) noexcept;
~future();
future(future const&) = delete;
future& operator=(future const&) = delete;
bool valid() const noexcept;
ResultType get();
shared_future<ResultType> share();
void wait();
template<typename Rep,typename Period>
future_status wait_for(
std::chrono::duration<Rep,Period> const& relative_time);
template<typename Clock,typename Duration>
future_status wait_until(
std::chrono::time_point<Clock,Duration> const& absolute_time);
};
构造函数
- 不带参数的默认构造函数,此对象没有共享状态,因此它是无效的,但是可以通过移动赋值的方式将一个有效的future值赋值给它;
- 禁用拷贝构造;
- 支持移动构造。
-
std::future<int> fut; // 默认构造函数 fut = std::async(do_some_task); // move-赋值操作。
析构函数
-
销毁future对象,它是异常安全的。
get函数
- 当共享状态就绪时,返回存储在共享状态中的值(或抛出异常)。
- 如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。
- 当共享状态就绪后,则该函数将取消阻塞并返回(或抛出)释放其共享状态,这使得future对象不再有效,因此对于每一个future共享状态,该函数最多应被调用一次。
std::future<void>::get()
(void特例化)不返回任何值,但仍等待共享状态就绪并释放它。- 共享状态是作为原子操作(atomic operation)被访问。
operator=
-
禁用拷贝赋值。
-
支持移动赋值:如果在调用之前,此对象是有效的(即它已经访问共享状态),则将其与先前已关联的共享状态解除关联。如果它是与先前共享状态关联的唯一对象,则先前的共享状态也会被销毁。
share函数
-
获取共享的future,返回一个
std::shared_future
对象,该对象获取future对象的共享状态。调用该函数之后,该std::future
对象本身已经不和任何共享状态相关联,因此该std::future
的状态不再是 valid 的了。
valid函数
-
检查共享状态的有效性,返回当前的
future
对象是否与共享状态关联。一个有效的std::future
对象只能通过std::async()
,std::future::get_future
或者std::packaged_task::get_future
来初始化。另外由 std::future 默认构造函数创建的 std::future 对象是无效(invalid)的,当然通过 std::future 的 move 赋值后该 std::future 对象也可以变为 valid。一旦调用了std::future::get()
函数,再调用此函数将返回false。
wait函数
- 等待共享状态就绪。如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。
- 当共享状态就绪后,则该函数将取消阻塞并返回。但是
wait()
并不读取共享状态的值或者异常。
wait_for函数
- ready:共享状态已就绪;
- timeout:在指定的时间内未就绪;
- deferred:共享状态包含了一个延迟函数(deferred function)。
- 等待共享状态在指定的时间内(time span)准备就绪。如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪或已达到设置的时间。
- 此函数的返回值类型为枚举类
future_status,
此枚举类有三种label。
wait_until函数
- 等待共享状态在指定的时间点(time point)准备就绪。如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪或已达到指定的时间点。
- 此函数的返回值类型为枚举类
future_status
。
代码示例
#include <iostream> // std::cout
#include <future> // std::async, std::future
#include <chrono> // std::chrono::milliseconds
// a non-optimized way of checking for prime numbers:
bool is_prime (int x) {
for (int i=2; i<x; ++i) if (x%i==0) return false;
return true;
}
int main ()
{
// call function asynchronously:
std::future<bool> fut = std::async (is_prime,444444443);
// do something while waiting for function to set future:
std::cout << "checking, please wait";
std::chrono::milliseconds span (100);
while (fut.wait_for(span)==std::future_status::timeout)
std::cout << '.' << std::flush;
bool x = fut.get(); // retrieve return value
std::cout << "\n444444443 " << (x?"is":"is not") << " prime.\n";
return 0;
}
- 使用
std::async
异步调用is_prime
函数,参数为444444443
。std::async
返回一个std::future
对象fut
,可以用来查询异步任务的状态或获取其返回值。使用std::async
异步执行任务,避免主线程阻塞。 - 使用一个
std::chrono::milliseconds
对象span
代表 100 毫秒的时间间隔。在一个循环中,每隔 100 毫秒检查一次异步任务的状态。如果任务还未完成(即返回std::future_status::timeout
),输出一个点符号,继续等待。 - 当异步任务完成后,使用
get()
方法获取其返回值。
std::promise
概念
std::promise
是一个模板类: template<typename ResultType> class promise
。其泛型参数ResultType
是std::promise
对象保存的值的类型,例如std::promise<int>
,ResultType
可以是void类型。std::promise
类型模板提供设置异步结果的方法,这样其他线程就可以通过std::future
实例来读取该结果。std::promise
和std::future
合作共同实现了多线程间通信。
在构造std::promise对象时,该对象与新的共享状态(shared state)关联。通过调用std::promise
的get_future
函数,可以将该共享状态与std::future
对象关联。调用get_future
之后,两个对象共享相同的共享状态:std::promise对象是异步提供程序(asynchronous provider),应在某个时刻为共享状态设置一个值。std::future
对象是个异步返回对象,可以检索共享状态的值,并在必要时等待其准备就绪。需要注意的是:set_value
只能被调用一次,多次调用会抛出std::future_error
异常。事实上std::promise::set_xxx
函数会改变std::promise的状态为ready,再次调用时发现状态已是reday了,则抛出异常。
std::promise
实例是可以MoveConstructible
(移动构造)和MoveAssignable
(移动赋值),但是不能CopyConstructible
(拷贝构造)和CopyAssignable
(拷贝赋值)。
成员函数
template<typename ResultType>
class promise
{
public:
promise();
promise(promise&&) noexcept;
~promise();
promise& operator=(promise&&) noexcept;
template<typename Allocator>
promise(std::allocator_arg_t, Allocator const&);
promise(promise const&) = delete;
promise& operator=(promise const&) = delete;
void swap(promise& ) noexcept;
std::future<ResultType> get_future();
void set_value(see description);
void set_exception(std::exception_ptr p);
};
构造函数
#include <iostream> // std::cout
#include <functional> // std::ref
#include <memory> // std::allocator, std::allocator_arg
#include <thread> // std::thread
#include <future> // std::promise, std::future
void print_int (std::future<int>& fut) {
int x = fut.get();
std::cout << "value: " << x << '\n';
}
int main ()
{
std::promise<int> foo;
std::promise<int> bar = std::promise<int>(std::allocator_arg,std::allocator<int>());
std::future<int> fut = bar.get_future();
std::thread th (print_int, std::ref(fut));
bar.set_value (20);
th.join();
return 0;
}
代码示例
#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>
void accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last,
std::promise<int> accumulate_promise)
{
int sum = std::accumulate(first, last, 0);
accumulate_promise.set_value(sum);
}
void do_work(std::promise<void> barrier)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
barrier.set_value();
}
int main()
{
// 用 promise<int> 在线程间传递结果
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
std::promise<int> accumulate_promise;
std::future<int> accumulate_future = accumulate_promise.get_future();
std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
std::move(accumulate_promise));
// future::get() 将等待直至该 future 拥有合法结果并取得它
// 无需在 get() 前调用 wait()
//accumulate_future.wait(); // 等待结果
std::cout << "result=" << accumulate_future.get() << '\n';
work_thread.join(); // wait for thread completion
// 用 promise<void> 在线程间对状态发信号
std::promise<void> barrier;
std::future<void> barrier_future = barrier.get_future();
std::thread new_work_thread(do_work, std::move(barrier));
barrier_future.wait();
}
-
accumulate
函数累加一个向量范围内的所有元素,并通过std::promise<int>
将结果传递出去; do_work
函数模拟一些工作,通过std::promise<void>
向线程发送信号,指示工作已经完成;- 启动一个线程
work_thread
,它执行accumulate
函数,该函数将计算结果传递给accumulate_promise;
- 使用
accumulate_future.get()
获取结果并输出,这里不需要显式调用wait()
,因为get()
会等待结果就绪。然后join
等待线程完成;
std::packaged_task
概念定义
std::packaged_task是一个模板类,它允许传入一个函数或其他可调用对象,并将函数计算的结果作为异步结果传递给std::future
,包括函数运行时产生的异常。
代码示例
#include <iostream>
#include <cmath>
#include <thread>
#include <future>
#include <functional>
// unique function to avoid disambiguating the std::pow overload set
int f(int x, int y) { return std::pow(x,y); }
void task_lambda()
{
std::packaged_task<int(int,int)> task([](int a, int b) {
return std::pow(a, b);
});
std::future<int> result = task.get_future();
task(2, 9);
std::cout << "task_lambda:\t" << result.get() << '\n';
}
void task_bind()
{
std::packaged_task<int()> task(std::bind(f, 2, 11));
std::future<int> result = task.get_future();
task();
std::cout << "task_bind:\t" << result.get() << '\n';
}
void task_thread()
{
std::packaged_task<int(int,int)> task(f);
std::future<int> result = task.get_future();
std::thread task_td(std::move(task), 2, 10);
task_td.join();
std::cout << "task_thread:\t" << result.get() << '\n';
}
int main()
{
task_lambda();
task_bind();
task_thread();
}
-
task_lambda
函数使用 lambda 表达式创建并调用一个std::packaged_task
。 -
task_bind
函数使用std::bind
创建并调用一个std::packaged_task
。 -
task_thread
函数将std::packaged_task
通过线程调用。
std::async
概念
std::async是函数模板。std::thread使得我们可以非常方便的创建线程,执行异步任务,它的基本用法是这样的:
#include <iostream>
int add(int a, int b) { return a + b;}
int main() {
std::thread t(add, 2, 3);
t.join();
}
这段代码新创建一个线程来计算两个数相加。但我们没办法获取线程的计算返回值。有时候使用thread有些不便,比如我希望获取线程函数的返回结果的时候,我就不能直接通过 thread.join()得到结果,这时就必须定义一个变量,在线程函数中去给这个变量赋值,然后join,最后得到结果,这个过程是比较繁琐的。为此,c++11提供了异步接口std::async
,通过这个异步接口可以很方便的获取线程函数的执行结果。std::async
会自动创建一个线程去调用线程函数,它返回一个std::future
,这个future中存储了线程函数返回的结果,当我们需要线程函数的结果时,直接从future中获取,非常方便。但是std::async
提供的便利不仅仅是这一点,它首先解耦了线程的创建和执行,使得我们可以在需要的时候获取异步操作的结果;其次它还提供了线程的创建策略(比如可以通过延迟加载的方式去创建线程),使得我们可以以多种方式去创建线程。
当调用std::async
返回一个包含任务结果的std::future
对象。根据策略,任务在其所在线程上是异步运行的,当有线程调用了这个future对象的wait()
和get()
成员函数,则该任务会同步运行。有点类似封装了thread
和packged_task
的功能,使异步执行一个任务更为方便。
成员函数
enum class launch
{
async, // 运行新线程来执行任务
deferred // 惰性求值,请求结果时才执行任务
};
template<typename Callable,typename ... Args>
future<result_of<Callable(Args...)>::type>
async(Callable&& func, Args&& ... args);
template<typename Callable,typename ... Args>
future<result_of<Callable(Args...)>::type>
async(launch policy, Callable&& func, Args&& ... args);
std::async
是一个函数而非类模板,其函数执行完后的返回值绑定给std::futrue
对象。func是要调用的可调用对象(function, member function, function object, lambda),Args是传递给Func的参数,std::launch policy
是启动策略,它控制std::async
的异步行为,我们可以用三种不同的启动策略来创建std::async
:
std::launch::async
参数 保证异步行为,即传递函数将在单独的线程中执行;std::launch::deferred
参数 当其他线程调用get()/wait()来访问共享状态时,将调用非异步行为;std::launch::async | std::launch::deferred
参数 是默认行为(可省略)。有了这个启动策略,它可以异步运行或不运行,这取决于系统的负载。
#include <future>
#include <iostream>
#include <thread>
#include <chrono>
int entry() {
std::cout <<"call entry" << std::endl;
return 11;
}
int main() {
std::future<int> the_answer=std::async(entry);
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "The answer is " << the_answer.get()<<std::endl;
}
与std::thread
方式一样,std::async
允许通过添加额外的调用参数,向函数传递额外的参数。第一个参数是指向成员函数的指针,第二个参数提供这个函数成员类的具体对象(是通过指针,也可以包装在std::ref
中),剩余的参数可作为函数的参数传入。否则,第二个和随后的参数将作为函数的参数,或作为指定可调用对象的第一个参数。和std::thread
一样,当参数为右值时,拷贝操作将使用移动的方式转移原始数据,就可以使用“只移动”类型作为函数对象和参数。
#include <iostream>
#include <thread>
#include <chrono>
#include <string>
#include <future>
struct X
{
void foo(int x, std::string const& str) {
std::cout << "foo: " << x << " " << str <<std::endl;
}
std::string bar(std::string const& str) {
std::cout << "bar: " << str <<std::endl;
return str;
}
};
void test_1() {
std::cout << "test_1" <<std::endl;
X x;
auto f1=std::async(&X::foo,&x, 42, "hello"); // 调用p->foo(42, "hello"),p是指向x的指针
auto f2=std::async(&X::bar, x, "goodbye"); // 调用tmpx.bar("goodbye"), tmpx是x的拷贝副本
}
struct Y
{
double operator()(double x) { return x; }
};
void test_2() {
std::cout << "test_2" <<std::endl;
Y y;
auto f3=std::async(Y(), 3.141); // 调用tmpy(3.141),tmpy通过Y的移动构造函数得到
std::cout << "test_2 f3: " << f3.get() <<std::endl;
auto f4=std::async(std::ref(y), 2.718); // 调用y(2.718)
std::cout << "test_2 f4: " << f4.get() <<std::endl;
}
X baz(X&) { return X(); }
void test_3() {
std::cout << "test_3" <<std::endl;
X x;
auto f=std::async(baz,std::ref(x)); // 调用baz(x)
f.get();
}
class move_only
{
public:
move_only() {std::cout << "default\n";}
move_only(move_only&&) {std::cout << "copy\n";}
move_only(move_only const&) = delete;
move_only& operator=(move_only&&) { std::cout << "operator=\n";return *this; }
move_only& operator=(move_only const&) = delete;
void operator()() {std::cout << "operator()\n";}
};
void test_4() {
std::cout << "test_4" <<std::endl;
// move_only()
// 然后是std::move(move_only())构造得到
// 最后是operator()
auto f5=std::async(move_only());
}
int main() {
test_1();
test_2();
test_3();
test_4();
std::cout << "finsh!" <<std::endl;
}
注意
一
std::async 的返回值(std::future)在析构函数里会等待任务完成,如果不注意 std::async 的返回值很有可能就没有异步效果。
#include <iostream>
#include <chrono>
#include <future>
std::time_t now()
{
auto t0 = std::chrono::system_clock::now();
std::time_t time_t_today = std::chrono::system_clock::to_time_t(t0);
return time_t_today; // seconds
}
void print() {
std::cout << now() << " print start!" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << now() << " print end!" << std::endl;
}
int main() {
{
std::async(std::launch::async, print);
}
// wait the print finish here
{
auto f = std::async(std::launch::async, print);
}
// wait the print finish here
std::cout << now() << " all finsh!" <<std::endl;
}
二
使用默认策略却很方便,它不需要你显示指定的,或者你可以显示指定为std::launch::async | std::launch::deferred
,c++标准中给出的说明是:进行异步执行还是惰性求值取决于实现:
auto future = std::async(func); // 使用默认发射模式执行func
这种调度策略我们没有办法预知函数func是否会在哪个线程执行,甚至无法预知会不会被执行,因为func可能会被调度为推迟执行,即调用get或wait的时候执行,而get或wait是否会被执行或者在哪个线程执行都无法预知。
同时这种调度策略的灵活性还会混淆使用thread_local变量,这意味着如果func写或读这种线程本地存储(Thread Local Storage,TLS),预知取到哪个线程的本地变量是不可能的。
它也影响了基于wait循环中的超时情况,因为调度策略可能为deferred
的,调用wait_for或者wait_until会返回值std::launch::deferred。
std::async意义
在已经有了td::future、std::promise和std::packaged_task的情况下,实现异步或多线程间通信,可能觉得已经足够了,真的还要一个std::async来凑热闹吗?是std::async是为了让用户的少费点脑子的,它让这三个对象默契的工作。大概的工作过程是这样的:std::async先将异步操作用std::packaged_task包装起来,然后将异步操作的结果放到std::promise中,这个过程就是创造未来的过程。外面再通过future.get/wait来获取这个未来的结果。std::async真的是来帮忙的,你不用再想到底该怎么用std::future、std::promise和 std::packaged_task了,std::async已经帮你搞定一切了!这就是我们前面说的,std::async类似封装了thread
和packged_task
的功能。使得我们使用起来更加方便简单。