admin管理员组

文章数量:1534208

C++11提供了三种异步操作方式:std::async, std::packaged_task和std::promise。他们都定义在头文件<future>下,使用时都可以得到一个std::future对象,用于和异步操作进行通信。

这三者的区别:

  • std::async最简洁,返回值就是future对象,任务可以在调用std::async结束后立即执行,也可以延后执行,使用future来控制
  • std::packaged_task把future对象的创建和任务的执行分割开来,提升了自由度,任务可以放到指定线程中去运行
  • std::promise灵活性最高,std::async和std::packaged_task都必须等到任务执行完毕后才能在future对象里拿到返回值,std::promise则可以在任务执行的任意时候都能拿到返回值,不必等到任务执行结束。

一 std::async

cppreference上定义如下,

The function template async runs the function f asynchronously (potentially in a separate thread which might be a part of a thread pool) and returns a std::future that will eventually hold the result of that function call.

这个potentially用的妙,std::async执行的任务可以运行在单独线程里,也可以不运行在单独线程里,如果是立即执行则会开一个线程去执行,如果是延后执行,则会在调用任务的线程里阻塞执行。

1. 立即执行

#include <future>
#include <iostream>


bool myfunc(int data1, int data2)
{
    std::cout << "myfunc id: " << std::this_thread::get_id() << "\n";
    
    if (data1+data2 > 100)
    {
        return true;
    }
    else
    {
        return false;
    }
}

int main(void)
{
    std::cout << "main id: " << std::this_thread::get_id() << "\n";
    
    std::future<bool> fu = std::async(myfunc, 100, 200); // 100和200是myfunc需要的参数
    
    // 阻塞等待
    fu.get();

    return 0;
}

编译运行后,结果如下,

可以看出线程id不同,说明是单独开了一个线程去执行的。

2. 延后执行

如果传给std::async的第一个参数是std::launch::defered,那么任务就可以延后执行,什么时候执行呢?当调用future的get()方法时就会执行,但是不会开线程去执行。

#include <future>
#include <iostream>


bool myfunc(int data1, int data2)
{
    std::cout << "myfunc id: " << std::this_thread::get_id() << "\n";
    
    if (data1+data2 > 100)
    {
        return true;
    }
    else
    {
        return false;
    }
}

int main(void)
{
    std::cout << "main id: " << std::this_thread::get_id() << "\n";
    
    std::future<bool> fu = std::async(std::launch::deferred, myfunc, 100, 200); // 100和200是myfunc需要的参数
    
    // 等待1秒
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 阻塞等待
    fu.get();

    return 0;
}

编译运行后,结果如下,

可以看出2个id值是一样的,说明是在同一个线程,不过这样的话感觉就不是异步执行了…

3. future对象的析构问题

如果是立即执行,那么还需要注意future对象的析构问题。

如果要析构std::async返回的future对象,那么future对象的析构函数会阻塞当前线程,直到任务执行结束。

#include <future>
#include <iostream>


bool myfunc(int data1, int data2)
{
    std::cout << "myfunc id: " << std::this_thread::get_id() << "\n";
    
    // 等待1秒
    std::this_thread::sleep_for(std::chrono::seconds(1));

    if (data1+data2 > 100)
    {
        return true;
    }
    else
    {
        return false;
    }
}

int main(void)
{
    std::cout << "main id: " << std::this_thread::get_id() << "\n";
    
    { // 作用域开始
        std::future<bool> fu = std::async(myfunc, 100, 200); // 100和200是myfunc需要的参数
    } // 作用域结束
    

    return 0;
}

这里使用花括号来定义一个作用域,这样当fu出了作用域后会被释放,就会去调用析构函数,但是myfunc里有个延时1秒的等待,这样fu就会阻塞main函数所在的线程,一直到myfunc执行结束。


二 std::packaged_task

相比于std::async,std::packaged_task自由度更高,而且也没有future对象析构阻塞的问题。

cppreference上定义如下,

The class template std::packaged_task wraps any Callable target (function, lambda expression, bind expression, or another function object) so that it can be invoked asynchronously. Its return value or exception thrown is stored in a shared state which can be accessed through std::future objects.

分割了future对象的创建和任务的执行,执行任务可以显示地指定某个线程去做,不像std::async是背后做的。

例子如下,

#include <future>
#include <iostream>


bool myfunc(int data1, int data2)
{    
    // 等待1秒
    std::this_thread::sleep_for(std::chrono::seconds(1));

    if (data1+data2 > 100)
    {
        return true;
    }
    else
    {
        return false;
    }
}

int main(void)
{
    std::packaged_task<bool(int, int)> task(myfunc);

    std::future<bool> fu = task.get_future();
    
	// 必须要使用std::move把task转成右值
    std::thread t(std::move(task), 100, 200); // 100和200是myfunc需要的参数
    
	// 调用get进行阻塞等待
    std::cout << fu.get() << "\n";
    
    t.join();

    return 0;
}

因为packaged_task是个wrapper,所以其类型就是被打包的类型。例子中myfunc的返回类型是bool,参数是2个int,那么packaged_task的类型就是bool(int, int)。然后调用get_future()方法得到future对象,类型是被打包的对象的返回值。


三 std::promise

相比于前2个,std::promise不需要等到任务结束就可以拿到future对象里的值,而且自由度最高,可以根据需要来决定返回值类型,不必是任务结束时的返回值。

另外,std::promise只是个异步结果的提供者,不像前2者可以传递任务进来。

cppreference上定义如下,

The class template std::promise provides a facility to store a value or an exception that is later acquired asynchronously via a std::future object created by the std::promise object. Note that the std::promise object is meant to be used only once.

Each promise is associated with a shared state, which contains some state information and a result which may be not yet evaluated, evaluated to a value (possibly void) or evaluated to an exception.

简单例子

下面是个例子,

#include <future>
#include <iostream>
#include <string>


bool myfunc(std::promise<std::string> pro, int data1, int data2)
{    
    // 等待1秒
    std::this_thread::sleep_for(std::chrono::seconds(1));

    pro.set_value("hello");

    // 等待2秒
    std::this_thread::sleep_for(std::chrono::seconds(2));

    if (data1+data2 > 100)
    {
        return true;
    }
    else
    {
        return false;
    }
}

int main(void)
{
    std::promise<std::string> pro;

    std::future<std::string> fu = pro.get_future();
	
    // 必须使用std::move把pro转成右值,之后pro就不能再被使用
    std::thread t(myfunc, std::move(pro), 100, 200);

    std::cout << fu.get() << "\n";
    
    t.join();

    return 0;
}

例子中不关心myfunc的返回值,而是自定义的一个目标,类型是std::string。另外,需要把pro传给myfunc,这样myfunc的参数就需要修改下,增加std::promise类型的参数。

在myfunc中等待1s后调用std::promise的set_value()方法设置值(注意值类型要和std::promise参数的模板类型一样,这里是std::string),然后在main线程中,fu.get()就可以拿到这个值了,此时myfunc还没结束运行。

void类型

std::promise的模板类型也可以是void,这样std::promise就只起到一个通知作用,不需要关心返回值类型。下面是个例子,

#include <future>
#include <iostream>
#include <string>


bool myfunc(std::promise<void> pro, int data1, int data2)
{    
    // 等待1秒
    std::this_thread::sleep_for(std::chrono::seconds(1));

    pro.set_value();

    // 等待2秒
    std::this_thread::sleep_for(std::chrono::seconds(2));

    if (data1+data2 > 100)
    {
        return true;
    }
    else
    {
        return false;
    }
}

int main(void)
{
    std::promise<void> pro;

    std::future<void> fu = pro.get_future();

    std::thread t(myfunc, std::move(pro), 100, 200);

    fu.get();

    std::cout << "XXX\n";
    
    t.join();

    return 0;
}

例子中,std::promise和std::future都是void类型,在myfunc中等待1s后调用set_value()但是不需要传值进去,这样在主线程中fu.get()的阻塞就会打开,代码继续往后执行。

而且不能用std::cout去输出fu.get()了。

互换位置

有时可能想用主线程去控制其它线程,那么可以把std::promise和std::future所在的位置互换一下,例子如下,

#include <future>
#include <iostream>
#include <string>


bool myfunc(std::future<void> fu, int data1, int data2)
{    
    fu.get();

    std::cout << "DDD\n";

    if (data1+data2 > 100)
    {
        return true;
    }
    else
    {
        return false;
    }
}

int main(void)
{
    std::promise<void> pro;

    std::future<void> fu = pro.get_future();

    std::thread t(myfunc, std::move(fu), 100, 200);

    // 等待1s
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 进行通知
    pro.set_value();
    
    t.join();

    return 0;
}

这里把std::promise留在了main函数中,std::future传给了myfunc,然后myfunc会等待main的指令。

有点类似于条件变量…


四 总结

本文讲述了std::async, std::packaged_task和std::promise的使用方式以及差异,可以根据需要进行选择使用。

本文标签: 三种区别操作方式