C++11 线程异步

1. 线程异步的概念

问题1: 如何理解线程异步?

 异步的反义词是同步。异步与同步的区别见此处: [[Linux/计算机网络基础知识点/高级IO#同步通信 vs 异步通信]]。实际上在多线程下,大部分时候都是存在过异步这一状态的。主线程在创建了子线程后,也去干自己的任务了。

问题2: 线程异步的应用场景?

  1. 主线程想要得到某一子线程运行的任务函数的运行结果。这里的结果可以使用future对象进行存储。(future是个模板类, 能存储任意类型, 包括void) //子—>主
  2. 主线程想要通知子线程, 依靠future值和状态 来达成某一目的(让子线程结束/满足条件/…), 此时主线程在外面设置future对象的共享状态及future值。子线程那边可以根据future对象的状态和值进行一些逻辑判断然后到达想要的结果。 //主—>子

2. future

//包含于<future>头文件
template <class T>  future;
template <class R&> future<R&>;     // specialization : T is a reference type (R&)
template <>         future<void>;   // specialization : T is void
//---------------------------------------------------
//构造函数
future() noexcept;					//(1) default
future (const future&) = delete;	//(2) copy [deleted]
future (future&& x) noexcept;		//(3) move

//赋值
future& operator=(future&& other) noexcept;
future& operator=(const future& other) = delete;


 future对象是用于存储某一类型<class T>的值的,只不过这个值往往是在未来才能获取到。 它被用来作为线程异步的中间存储值。future的值由以下三个异步任务的提供者(Provider)提供:

  1. std::promise
  2. std::package_task
  3. std::async

 我们根据future的构造函数可以发现,future不支持拷贝构造。future的operator=()会去调用移动构造。

2.1 共享状态

 future在线程异步当中扮演的是一个被动角色。它需要与promisepackage_taskasync配合来实现线程异步。由于它必须要进行共享关联,因此future对象时存在共享状态是否有效的问题的。只有共享状态有效,才能获取future的值

 future对象是有"共享状态"这一概念的。共享状态必须依靠上面提到的三者对应的方法: promise::get_future()package_task::get_future()async()获取。否则单纯的创建一个future对象, 它的共享状态是无效的!

共享状态解释
future_status::deferred子线程中的任务函仍未启动
future_status::timeout子线程中的任务正在执行中,指定等待时长已用完
future_status::ready子线程中的任务已经执行完毕结果已就绪

 实际上,为了方便我们理解,还应该加一个状态: 无效状态(invalid)。这个状态存在于:
①future对象没有接收任何提供者的共享关联;
②future对象ready完毕后,被调用者通过get()获取过了。

2.2 常用成员函数

成员函数功能
valid()判断共享状态是否有效
wait()等待共享状态ready
wait_for()等待一段时间
wait_until()等待到某个时间点
get()获取future的值

注意:

  • 在调用get()时,如果future的共享状态不是ready, 则调用者会被阻塞。
  • get()只能被调用一次,第二次会抛出异常。(因为第一次完成后,future的状态就是无效的了)
  • 调用wait()方法会阻塞式等待共享状态为ready。
  • wait_for()wait_until()无法保证等待结束后的future对象的状态一定是ready! (所以它们不太常用, 因为调用完毕后还需要使用valid()判断共享状态)
  • wait_for()wait_until()的返回值是std::future_status。因此我们可以通过接收它们的返回值来循环判断future对象是否ready

3. promise

//包含于<future>头文件
template <class T>  promise;
template <class R&> promise<R&>;  // specialization : T is a reference type (R&)
template <>         promise<void>;// specialization : T is void

//构造函数
promise();								//(1)
promise(promise&& other) noexcept;		//(2) 移动构造
promise(const promise& other) = delete;	//(3) 禁止拷贝构造

//赋值
promise& operator= (promise&& rhs) noexcept; //允许移动赋值
promise& operator= (const promise&) = delete;//禁止拷贝赋值

 promise是一个协助线程赋值的类,在promise类的内部管理着一个future对象。因此它能够提供一些将数据和future对象绑定起来的接口。

3.1 常用成员函数

成员函数功能
get_future()获取future对象
set_value()设置future对象的值(立刻)
set_value_at_thread_exit()在线程结束时,才会设置future对象的值,


• get_future()

 get_future()会返回一个future对象, 此时如果去接收它的返回值则会触发移动赋值, 将资源转移。

• set_value()

 设置future对象的值,并立即设置future对象的共享状态为ready

• set_value_at_thread_exit()

 设置future对象的值,但是不会立刻让future对象的共享状态为ready。在子线程退出时,子线程资源被销毁,再令共享状态为ready

3.2 promise的基本使用

①: 子线程 set_value—> 给主线程

  1. 在主线程中创建promise对象
  2. 将这个promise对象通过引用传递的方式传给子线程的任务函数(ref)
  3. 子线程合适的时候调用set_value()方法, 设置future对象的值以及状态(ready)
  4. 主线程通过调用promise对象中的get_future()方法获取到future对象 (这里是移动构造了)
  5. 主线程调用future对象中的get()方法获取到子线程set_value()所设置的值。
void func(promise<int>& pr)
{
    cout << "Child Thread Working~~~" << endl;
    cout << "Child Thread: Waiting 3 seconds!" << endl;
    this_thread::sleep_for(chrono::seconds(3));
    
    pr.set_value(3);
    this_thread::sleep_for(chrono::seconds(1));
    cout << "Child Exit" << endl;
}

int main()
{
    promise<int> pr;
    thread t(func, ref(pr));
    auto f = pr.get_future();
    this_thread::sleep_for(chrono::seconds(1));
    cout << "Get Future: " << f.get() << endl;
    t.join();
    return 0;
}

注意:

 根据现象, 我们可以发现主线程在调用f.get()时阻塞了一会。此时说明子线程还没有执行到set_value(), 此时的future对象中的共享状态不是ready, 因此主线程会被阻塞。


②: 主线程 set_value–> 给子线程

  1. 在主线程中创建promise对象
  2. 将这个promise对象通过引用传递的方式传给子线程的任务函数(ref)
  3. 主线程合适的时候调用set_value()方法, 设置future对象的值以及状态(ready)
  4. 在编码子线程时,设置依future对象的值的判断条件,当future的共享状态或者值满足条件时,执行某一任务(或终止)
void func2(promise<int>& pr)
{
    int i = 0;
    auto val = pr.get_future().get();
    if(val == 1){
        cout << "Get Value: " << val << endl;
        //do something
    }
    else{
        cout << "Get Value: " << val << endl;
        //do something
    }
}

int main()
{
    promise<int> pr;
    thread t(func2, ref(pr));
    cout << "Main Thread: Waiting 3 seconds!" << endl;
    this_thread::sleep_for(chrono::seconds(3));
    pr.set_value(1);
    t.join();
}

输出:

Main Thread: Waiting 3 seconds!
Get Value: 1

4. package_task

//包含于<future>头文件
template <class T> packaged_task;     // undefined
template <class Ret, class... Args> class packaged_task<Ret(Args...)>;

//构造函数
packaged_task() noexcept;						//default (1)
template <class Fn>
  explicit packaged_task (Fn&& fn);				//initialization (2)
packaged_task (const packaged_task&) = delete;	//copy [deleted] (3)
packaged_task (packaged_task&& x) noexcept;		//move (4)

//赋值
packaged_task& operator=(packaged_task&& rhs) noexcept; //move (1)
packaged_task& operator=(const packaged_task&) = delete;//copy [deleted] (2)

 package_task包装了一个函数对象(类似于function), 我们可以把它当做函数对象来使用。package_task可以将内部包装的函数和future绑定到一起,以便于进行后续的异步调用。因此我们可以将其理解为它自带了一个函数,并且该函数和future对象绑定到了一起,我们不需要额外定义函数方法了,直接实现package_task中的函数对象即可。

 但package_task相比于promise有个缺点,它里面包装了函数,而该函数的返回值就是future对象的值。它无法像使用promise那样灵活。

4.1 常用成员函数

 package_task中最常用的就是get_future()方法了。它能够获取到package_task中的future对象。

4.2 package_task的基本使用

 将package_task作为线程的启动函数传过去,传参方式必须是引用传递 ref()

int main()
{
    packaged_task<int(int, int)> pt_Add([](int x, int y)
    {
        cout << "Running~~~~~~~~~~" << endl;
        this_thread::sleep_for(chrono::seconds(3));
        return x + y;
    });

    future<int> fi = pt_Add.get_future();

    cout << "Start Thread!" << endl;
    thread t(ref(pt_Add), 10, 20);

    cout << "before get" << endl;
    int val = fi.get();
    cout << "val: " << val << endl;
    t.join();
    return 0;
}

输出:

Start Thread!
before get
Running~~~~~~~~~~
val: 30

5. async

//构造函数
// (1)
template<class Fn, class... Args>
future<typename result_of<Fn(Args...)>::type>
    async (Fn&& fn, Args&&... args);

// (2)
template<class Fn, class... Args>
future<typename result_of<Fn(Args...)>::type>
    async (launch policy, Fn&& fn, Args&&... args);	//policy是启动策略

 async是一个函数,它相比于前面的promise和package_task要高级一些。async可以直接启动一个子线程,然后让这个子线程执行对应的任务函数,任务函数的返回值就会被存储到future对象当中,future对象也就是async函数的返回值。主线程只需要接收asnyc的返回值,然后调用get()方法即可获取到future对象中保存的值。

: 更高级并不代表更好用,只是它的集成度更高一些,省去了我们要自己创建线程的步骤。async仍然有package_task的缺点,它无法像promise那样自由控制future在何时赋值。

• launch policy启动策略

策略解释
std::launch::async调用async函数时会创建新的线程,让该线程执行任务函数
std::launch::deferred调用async函数时不会创建新的线程,也不会去执行该任务函数。只有调用了async返回的future的get()方法或者wait()方法时才会去执行任务。(执行该任务的是主线程)

5.1 async的基本使用

• 使用默认的启动策略 — 调用async创建子线程, 并让该线程去执行任务

int main()
{
   future<int> f = async([](int x, int y)
   {
       cout << "Child Thread: Waiting 3 seconds!" << endl;
       this_thread::sleep_for(chrono::seconds(3));
       return x + y;
   }, 10, 20);

   this_thread::sleep_for(chrono::seconds(1));
   cout << "Get Value: " << f.get() << endl;
   return 0;
}

输出:

Child Thread: Waiting 3 seconds!
Get Value: 30


• 使用deferred启动策略 — 调用async不创建子线程

int main()
{
    future<int> f = async(launch::deferred, [](int x, int y)
    { 
        cout << "Child Thread "<< this_thread::get_id() << ": Waiting 3 seconds!" << endl;
        this_thread::sleep_for(chrono::seconds(3));
        return x + y;
    }, 10, 20);

    cout << "Main Thread "<< this_thread::get_id() <<": Working!!!" << endl;
    auto val = f.get();
    cout << "Get Value: " << val << endl;
	return 0;
}

输出:

Main Thread 1: Working!!!
Child Thread 1: Waiting 3 seconds!
Get Value: 30

 我们可以发现使用deferred策略时,是不会创建新的线程的。也就是说async的任务函数依然是由主线程自己去执行的,只不过执行的时机可以控制 (在调用get()方法时会去执行),这个机制类似于回调函数,你主动去调用get()才会去回调执行async的任务

6. promise、package_task、async的对比与总结

  1. promise类的使用相对灵活,但是需要自己创建线程,并且需要自己写一个函数对象。
  2. package_task类受限于只能使用函数返回值作为future对象的值。使用它也需要自己创建线程,但不需要额外写函数对象,直接将package_task当做函数对象去使用即可。
  3. async类集合度较高,它也受限于只能使用函数返回值作为future对象的值。但是async定义时可以自动创建线程,并让线程执行async中的任务函数。async的使用最简单,但是自由度较低。

细节总结:

  1. 调用get_future()方法, 并不会让线程被阻塞。只要调用future对象的get()方法,才可能被阻塞。(future共享状态没有ready就会被阻塞, 前提是future共享状态有效)
  2. 创建线程时,给线程传参要注意使用ref()的时机。
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++ 中,多线程异步回调函数可以通过使用 std::thread 和 std::async 实现。 std::thread 是 C++11 中引入的一个线程库,它可以创建一个新的线程来执行函数。例如,以下代码创建了一个新线程并执行了一个函数: ``` void myFunction() { // 执行一些操作 } int main() { std::thread t(myFunction); t.join(); // 等待线程执行完毕 return 0; } ``` std::async 也是 C++11 中引入的另一个库,它可以异步地执行一个函数并返回一个 std::future 对象,该对象可以用于获取异步函数的返回值。例如,以下代码使用 std::async 异步地执行一个函数: ``` int myFunction() { // 执行一些操作 return 42; } int main() { std::future<int> result = std::async(std::launch::async, myFunction); // 执行一些其他操作 int value = result.get(); // 等待异步函数执行完毕并获取返回值 return 0; } ``` 在多线程中使用异步回调函数时,可以将回调函数作为参数传递给异步函数,在异步函数完成后调用回调函数。例如,以下代码异步执行一个函数并在完成后调用回调函数: ``` void myCallback(int result) { // 处理异步函数的结果 } void myFunction(std::function<void(int)> callback) { // 执行一些操作 int result = 42; callback(result); // 调用回调函数 } int main() { std::function<void(int)> callback = myCallback; std::async(std::launch::async, myFunction, callback); // 执行一些其他操作 return 0; } ``` 在上面的代码中,myFunction 接受一个 std::function 对象作为回调函数,并在异步执行完毕后调用该函数。在 main 函数中,我们创建了一个 std::function 对象并将其传递给异步函数。在异步函数执行完毕后,myCallback 函数将被调用,并传递异步函数的结果作为参数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值