C++ 多线程:std::async

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

概念

我们之前的文章介绍过了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::async

std::async能够简单的使用可用的硬件并行来运行自身包含的异步任务。当调用std::async返回一个包含任务结果的std::future对象。根据策略,任务在其所在线程上是异步运行的,当有线程调用了这个future对象的wait()get()成员函数,则该任务会同步运行。有点类似封装了threadpackged_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;
}
复制代码

下面我们也看下带policy情况:

#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;
    }
};

struct Y
{
  double operator()(double x) { 
      std::cout << "Y operator(): " << x << std::endl;
      return x; }
};

X baz(X&) {
    std::cout << "call baz()" << std::endl;
    return X(); 
    }

int main() {
    // 在新线程上执行
    auto f6=std::async(std::launch::async, Y(), 1.2);

    X x;
    // 在wait()或get()调用时执行
    auto f7=std::async(std::launch::deferred, baz, std::ref(x));

    // 执行方式由系统决定
    auto f8=std::async(
                std::launch::deferred | std::launch::async,
                baz, std::ref(x));  
    // 执行方式由系统决定
    auto f9=std::async(baz, std::ref(x));

    f7.wait();  //  调用延迟函数

    std::cout << "finsh!" <<std::endl;
}
复制代码

注意事项

注意1

std::async 的返回值(std::future)在析构函数里会等待任务完成,如果不注意 std::async 的返回值很有可能就没有异步效果,看下原文是这样说的:

If the std::future obtained from std::async is not moved from or bound to a reference, the destructor of the std::future will block at the end of the full expression until the asynchronous operation completes, essentially making code such as the following synchronous:

std::async(std::launch::async, []{ f(); }); // temporary's dtor waits for f() std::async(std::launch::async, []{ g(); }); // does not start until f() completes

#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;
}
复制代码

输出:

1648365543 print start!
1648365544 print end!
1648365544 print start!
1648365545 print end!
1648365545 all finsh!
复制代码

注意2:默认policy

使用默认策略却很方便,它不需要你显示指定的,或者你可以显示指定为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。这意味着下面的循环,看起来最终会停止,但是,实际上可能会一直运行:

#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(2));
    std::cout << now() << " print end!" << std::endl;
}


int main() {
    // auto future = std::async(print);  // (概念上)异步执行f
    auto future = std::async(std::launch::deferred, print);  // 模拟默认policy时系统选择deferred的情况

    // 系统选择policy是deferred,那么下面这个循环将一直进行不会退出
    while(future.wait_for(std::chrono::seconds(1)) != std::future_status::ready)
    {
        std::cout << now() << " no ready!" <<std::endl;
    }

    std::cout << now() << " all finsh!" <<std::endl;
}
复制代码

为避免陷入死循环,我们必须检查future是否把任务推迟,然而future无法获知任务是否被推迟,一个好的技巧就是通过wait_for(0)来获取future_status是否是deferred,或者用do while里面把status全部进行判断做单独处理:

int main() {
    // auto future = std::async(print);  // (概念上)异步执行f
    auto future = std::async(std::launch::deferred, print);  // 模拟默认默认policy时系统选择deferred的情况

    std::future_status status;
    do {
        status = future.wait_for(std::chrono::seconds(1));
        if (status == std::future_status::deferred) {
            // 先判断系统是否选择policy是deferred,防止死循环
            // 可以直接break,后直接调get
            std::cout << "deferred\n";
            break;
        } else if (status == std::future_status::timeout) {
            std::cout << "timeout\n";
        } else if (status == std::future_status::ready) {
            std::cout << "ready!\n";
        }
    } while (status != std::future_status::ready); 

    future.get();
    std::cout << now() << " all finsh!" <<std::endl;
}
复制代码

存在意义

在已经有了td::future、std::promise和std::packaged_task的情况下,实现异步或多线程间通信,可能觉得已经足够了,真的还要一个std::async来凑热闹吗,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类似封装了threadpackged_task的功能。使得我们使用起来更加方便简单。

总结:

std::async可以理解为是更高层次上的异步操作,使我们不用关注线程创建内部细节,就能方便的获取异步执行状态和结果,还可以指定线程创建策略,它是对线程更高层次的抽象,可以优先选用std::async,但它是不能完全代替thread的,比如下面这个就无法用std::async代替:

std::thread([]
{
    // do other things
}).detach();
复制代码

另外,对于std::async,要优先使用std::async的默认策略,除非不满足上述使用条件,这会给予标准库更大的线程管理弹性。除非确实需要异步,才指定std::launch::async策略。

如果std::async满足不了使用需求,则使用std::thread,如:

  • 需要访问底层线程实现的API,如pthread库,设置线程优先级和亲和性。std::thread提供了native_handle成员函数
  • 需要且能够为应用优化线程用法,如执行时的性能剖析情况已知,且作为唯一的主要进程部署在一种硬件特性固定的平台上
  • 需要实现超越c++并发API的线程技术,如在c++实现中未提供的线程池的平台上实现线程池

参考

std::async - cppreference.com

(原创)用C++11的std::async代替线程的创建 - 南哥的天下 - 博客园 (cnblogs.com)

《Effective Modern C++》

猜你喜欢

转载自juejin.im/post/7083275361420574728