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()的时机。
<think>嗯,用户想了解C++11多线程异步操作的使用方法和示例代码。首先,我需要回忆一下C++11引入的多线程相关库,比如<thread>, <future>, <async>这些头文件。用户提到的异步操作,可能主要涉及std::async和std::future这两个类。 根据引用[1]中的内容,用户提到C++11线程类虽然方便,但获取线程返回结果比较麻烦,需要全局变量之类的。而C++提供的线程库中有类可以访问异步操作的结果,这里应该指的是std::future和std::async的组合。所以,我应该重点介绍如何使用std::async来启动异步任务,并通过std::future获取结果。 接下来,用户可能需要一个简单的示例代码,说明如何启动异步任务并获取返回值。比如,一个计算平方的函数,用std::async来调用,然后用future的get()方法获取结果。同时,要注意是否需要传递参数,比如值传递还是引用传递的问题。不过,在异步任务中,通常参数是按值传递的,除非用std::ref包裹,但需要谨慎处理生命周期问题,避免悬垂引用。 然后,用户可能还关心如何处理异常。因为如果在异步任务中抛出异常,future的get()会重新抛出这个异常,所以应该在调用get()时进行异常处理,比如try-catch块。 另外,引用[2]中提到了使用joinable来安全管理线程,但使用std::async的话,返回的future在析构时会自动等待任务完成,所以可能不需要手动join。不过,这可能取决于启动策略,比如std::launch::async还是std::launch::deferred。需要解释不同的启动策略对线程执行的影响。 此外,用户可能想知道如何传递参数给异步任务。比如,如果函数需要引用参数,必须使用std::ref,但要注意参数的生命周期必须足够长,避免线程还在运行时参数已经被销毁。例如,在示例代码中,如果传递一个局部变量的引用,需要确保线程在变量有效期内完成,或者使用其他方式管理参数。 还有,用户的问题可能涉及多个异步任务的并行执行,比如使用多个future对象,或者使用std::shared_future。这部分是否需要提及呢?可能根据用户的需求,但用户的问题主要是使用方法和示例,所以可能先给出基础示例,再扩展其他情况。 另外,错误处理也是关键,比如检查future是否有效,处理可能的异常。比如在示例中加入try-catch块,展示如何处理异步任务中的异常。 总结下来,回答的结构应该是先介绍std::async和std::future的基本用法,给出一个简单示例,说明如何定义异步函数、启动异步任务、获取结果。然后讨论参数传递的注意事项,包括值传递和引用传递的使用方法。接着,可以提到异常处理的方法,以及不同的启动策略。最后,可以提到更高级的用法,比如结合多个future或者使用packaged_task和promise,但可能用户只需要基础内容,所以作为扩展提及即可。 在示例代码中,需要注意代码的正确性,比如正确的头文件包含、命名空间的使用(比如using namespace std;),以及正确的语法,比如使用std::ref来传递引用参数。同时,注释说明每一步的作用,帮助用户理解代码逻辑。 还需要注意用户提供的引用内容中的例子,比如引用[2]中的线程函数如何传递引用参数,使用std::ref。所以在异步任务中,如果需要传递引用参数,也需要类似的处理,但要注意生命周期的问题,这可能需要在示例中强调。 最后,根据用户要求生成相关问题,可能涉及异步线程的区别、异常处理的最佳实践、性能优化等。</think>### C++11多线程异步操作详解 #### 一、异步操作核心组件 C++11通过$<future>$和$<thread>$头文件提供异步操作支持,核心工具包括: 1. **std::async**:异步任务启动器 2. **std::future**:异步结果访问器 3. **std::promise**:显式结果设置器 4. **std::packaged_task**:任务包装器 #### 二、基础使用方法 ```cpp #include <future> #include <iostream> int computeSquare(int n) { return n * n; } int main() { // 启动异步任务(默认策略) std::future<int> result = std::async(computeSquare, 5); // 阻塞获取结果 std::cout << "计算结果:" << result.get() << std::endl; // 输出25 return 0; } ``` 该示例演示了最基本的异步计算模式,通过$std::async$启动任务,$std::future::get()$阻塞获取结果[^1]。 #### 三、参数传递规范 1. **值传递**(默认安全方式) ```cpp std::async([](int a, std::string b){ /*...*/ }, 10, "hello"); ``` 2. **引用传递**(需显式声明) ```cpp int data = 10; std::async([&](int a){ data = a; }, std::ref(data)); // 使用std::ref包裹 ``` #### 四、异常处理机制 ```cpp auto task = [](){ throw std::runtime_error("异步任务异常"); return 0; }; std::future<int> f = std::async(task); try { f.get(); } catch(const std::exception& e) { std::cerr << "捕获异常:" << e.what() << std::endl; } ``` $future::get()$会自动传播异步任务中抛出的异常[^1]。 #### 五、启动策略控制 ```cpp // 强制立即创建线程 auto f1 = std::async(std::launch::async, task); // 延迟执行(调用get时同步执行) auto f2 = std::async(std::launch::deferred, task); ``` 不同策略影响线程创建时机和执行方式[^2]。 #### 六、多任务协同示例 ```cpp #include <vector> #include <future> void parallelSum() { std::vector<std::future<int>> futures; for(int i=0; i<4; ++i) { futures.emplace_back(std::async([=]{ return (i+1)*100; })); } int total = 0; for(auto& f : futures) { total += f.get(); // 累加各异步任务结果 } std::cout << "总和:" << total << std::endl; // 输出1000 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值