C++11线程异步-创新互联
- 1. 线程异步的概念
- 2. future
- 2.1 共享状态
- 2.2 常用成员函数
- 3. promise
- 3.1 常用成员函数
- 3.2 promise的基本使用
- 4. package_task
- 4.1 常用成员函数
- 4.2 package_task的基本使用
- 5. async
- 5.1 async的基本使用
- 6. promise、package_task、async的对比与总结
问题1: 如何理解线程异步?
异步的反义词是同步。异步与同步的区别见此处: [[Linux/计算机网络基础知识点/高级IO#同步通信 vs 异步通信]]。实际上在多线程下,大部分时候都是存在过异步这一状态的。主线程在创建了子线程后,也去干自己的任务了。
问题2: 线程异步的应用场景?
- 主线程想要得到某一子线程运行的任务函数的运行结果。这里的结果可以使用
future
对象进行存储。(future是个模板类, 能存储任意类型, 包括void)//子—>主 - 主线程想要通知子线程, 依靠future值和状态 来达成某一目的(让子线程结束/满足条件/…), 此时主线程在外面设置future对象的共享状态及future值。子线程那边可以根据future对象的状态和值进行一些逻辑判断然后到达想要的结果。//主—>子
2. future
//包含于头文件
templatefuture;
templatefuture; // specialization : T is a reference type (R&)
template<> future; // 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对象是用于存储某一类型
的值的,只不过这个值往往是在未来才能获取到。 它被用来作为线程异步的中间存储值。future的值由以下三个异步任务的提供者(Provider)提供:
std::promise
std::package_task
std::async
我们根据future的构造函数可以发现,future不支持拷贝构造。future的operator=()
会去调用移动构造。
2.1 共享状态
future在线程异步当中扮演的是一个被动角色。它需要与promise
、package_task
、async
配合来实现线程异步。由于它必须要进行共享关联,因此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
//包含于头文件
templatepromise;
templatepromise; // specialization : T is a reference type (R&)
template<> promise;// 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
—>给主线程
- 在主线程中创建
promise
对象 - 将这个
promise
对象通过引用传递的方式传给子线程的任务函数(ref
) - 子线程在合适的时候调用
set_value()
方法, 设置future对象的值以及状态(ready) - 主线程通过调用
promise
对象中的get_future()
方法获取到future对象 (这里是移动构造了) - 主线程调用
future
对象中的get()
方法获取到子线程set_value()
所设置的值。
void func(promise& 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()
{promisepr;
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
–>给子线程
- 在主线程中创建
promise
对象 - 将这个
promise
对象通过引用传递的方式传给子线程的任务函数(ref
) - 主线程在合适的时候调用
set_value()
方法, 设置future对象的值以及状态(ready) - 在编码子线程时,设置依
future
对象的值的判断条件,当future
的共享状态或者值满足条件时,执行某一任务(或终止)
void func2(promise& 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()
{promisepr;
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
//包含于头文件
templatepackaged_task; // undefined
templateclass packaged_task;
//构造函数
packaged_task() noexcept; //default (1)
templateexplicit 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_taskpt_Add([](int x, int y)
{cout<< "Running~~~~~~~~~~"<< endl;
this_thread::sleep_for(chrono::seconds(3));
return x + y;
});
futurefi = 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)
templatefuture::type>async (Fn&& fn, Args&&... args);
// (2)
templatefuture::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()
{futuref = 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()
{futuref = 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的对比与总结
- promise类的使用相对灵活,但是需要自己创建线程,并且需要自己写一个函数对象。
- package_task类受限于只能使用函数返回值作为future对象的值。使用它也需要自己创建线程,但不需要额外写函数对象,直接将package_task当做函数对象去使用即可。
- async类集合度较高,它也受限于只能使用函数返回值作为future对象的值。但是async定义时可以自动创建线程,并让线程执行async中的任务函数。async的使用最简单,但是自由度较低。
细节总结:
- 调用
get_future()
方法, 并不会让线程被阻塞。只要调用future对象的get()
方法,才可能被阻塞。(future共享状态没有ready就会被阻塞, 前提是future共享状态有效) - 创建线程时,给线程传参要注意使用
ref()
的时机。
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
新闻标题:C++11线程异步-创新互联
分享URL:http://myzitong.com/article/djgoje.html