并发机制
1 基于多线程并发
- C++11开始支持多线程编程,之前多线程编程都需要系统的支持,在不同的系统下创建线程需要不同的API如pthread_create(),Createthread(),beginthread()等。现在C++11中引入了一个新的线程库,C++11提供了新头文件,主要包含 、、、<condition_varible>、五个部分;等用于支持多线程,同时包含了用于启动、管理线程的诸多工具,同时,该库还提供了包括像互斥量、锁、原子量等在内的同步机制。
1.1 基础知识
C++多线程
- 线程:线程是操作系统能够进行CPU调度的最小单位,它被包含在进程之中,一个进程可包含单个或者多个线程。可以用多个线程去完成一个任务,也可以用多个进程去完成一个任务,它们的本质都相当于多个人去合伙完成一件事。
- 多线程并发多线程是实现并发(双核的真正并行或者单核机器的任务切换都叫并发)的一种手段,多线程并发即多个线程同时执行,一般而言,多线程并发就是把一个任务拆分为多个子任务,然后交由不同线程处理不同子任务,使得这多个子任务同时执行。
- C++多线程并发C++98标准中并没有线程库的存在,而在C++11中才提供了多线程的标准库,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类,。(简单情况下)实现C++多线程并发程序的思路如下:将任务的不同功能交由多个函数分别实现,创建多个线程,每个线程执行一个函数,一个任务就这样同时分由不同线程执行了。
相关的头文件说明
- thread头文件:存储thread线程与this_thread 命名空间的东西。基础实现
- future头文件:存储future、promise、async相关的类。高级实现
- mutex头文件:存储异步通信的相关的类
1.2 高级接口:Async与Future
头文件
#include<futrue>
future说明
标准库提供了一些工具来获取异步任务(即在单独的线程中启动的函数)的返回值,并捕捉其所抛出的异常。这些值在共享状态中传递,其中异步任务可以写入其返回值或存储异常,而且可以由持有该引用该共享态的 std::future 或 std::shared_future 实例的线程检验、等待或是操作这个状态。
定义于头文件 <future>
- promise存储一个值以进行异步获取(类模板)
- packaged_task打包一个函数,存储其返回值以进行异步获取(类模板)
- future等待被异步设置的值(类模板)
- shared_future等待被异步设置的值(可能为其他 future 所引用)(类模板)
- async异步运行一个函数(有可能在新线程中执行),并返回保有其结果的 std::future(函数模板)
- launch指定 std::async 所用的运行策略(枚举)
- future_status指定在 std::future 和 std::shared_future上的定时等待的结果(枚举)
Future 错误
- future_error报告与 future 或 promise 有关的错误(类)
- future_category鉴别 future 错误类别(函数)
- future_errc鉴别 future 错误码(枚举)
编程实例
#include<iostream>
#include<future>
#include<chrono>
#include<random>
#include<iostream>
#include<exception>
using namespace std;
int do_something(char c){
//初始化了一个随机数引擎和一个随机数分布
default_random_engine dre(c);
uniform_int_distribution<int> id(10,1000);
for(int i =0;i<10;++i){
//随机停止一段时间。
this_thread::sleep_for(chrono::milliseconds(id(dre)));
cout.put(c).flush();
}
return c;
}
int func1(){
return do_something('.');
}
int func2(){
return do_something('+');
}
int main(){
//启动异步线程,执行函数1。使用future作为占位符
//async的返回值与func1自动匹配,是模板函数。
//future object的类型也可以与async自动匹配,设置成auto result1()
//async接受任何可调用对象。包括函数、函数指针、lambda函数
future<int> result1(async(func1));
//主线程中执行函数2
int result2 = func2();
int result=0;
try
{
result = result1.get()+result2;
}
catch(const std::exception& e)
{
std::cerr << e.what() << '\n';
}
//计算结果,阻塞主线程
//输出结果
cout<<result<<endl;
return 0;
}
async与future说明
- sync的返回值与func1自动匹配,是模板函数。
- future object的类型也可以与async自动匹配,设置成auto result1()
- async接受任何可调用对象。包括函数、函数指针、lambda函数
future说明
函数名字 | 作用 |
---|
get | 调用future.get()函数会阻塞线程。等待另一个线程结束返回结果。如果不调用get函数。则main函数在结束前会等待这个线程结束并返回。get函数会捕获线程内的异常抛出,可以在get外捕获异常 |
valid | future.valid()检测线程是否处于正常运行状态还是已经退出。 |
wait | future.wait()函数会阻塞线程。但不需要获得返回结果。 |
wait_for | future.wait_for(std::chrono::seconds(10));等待最多10秒 |
wait_until | future.wait_until(system_clock::now()+chrono::minutes(1));等待当前时间后一分钟。 |
future_status说明
- wait_for和wait_until返回future status
常量 | 解释 |
---|
deferred | 共享状态含有延迟的函数,故将仅在显式请求时计算结果 |
ready | 共享状态就绪 |
timeout | 共享状态在经过指定的时限时长前仍未就绪 |
shared_future说明
- 其操作与future完全一致,可用于同时向多个线程发信。多个线程可以多次调用get函数。来获取线程执行结果。
1.3 底层接口:Thread与Promise
头文件
#include<thread>
Thread与future的区别
- future在一定程度上提供了线程通信和线程同步的方法。例如get可以获得另一个线程的返回值。wait()可以等待线程,实现线程同步。但是thread没有提供任何线程通信的方法。需要自己实现线程通信。(在操作系统部分,应该理解线程通信的原理和所有的方法)
- 异常无法在线程之间传递。
- 必须声明是同步线程join()还是一部线程detach()。future和async实现的线程是异步线程。可以使用get(),wait()进行同步。
- 如果线程运行与后台,main函数没有通过join等待线程结束,后台线程会被强制终止。
thread说明
函数 | 作用 |
---|
joinable | 检查线程是否可合并,即潜在地运行于平行环境中(公开成员函数) |
get_id | 返回线程的 id(公开成员函数) |
native_handle | 返回底层实现定义的线程句柄(公开成员函数) |
hardware_concurrency | [静态]返回实现支持的并发线程数(公开静态成员函数) |
函数 | 作用 |
---|
join | 等待线程完成其执行 |
detach | 容许线程从线程句柄独立开来执行 |
thread编程实现
#include<thread>
#include<chrono>
#include<random>
#include<iostream>
#include<exception>
using namespace std;
void doSomething(int num,char c){
try
{
default_random_engine dre(42*c);
uniform_int_distribution<int> id(10,1000);
for(int i=0;i<num;++i){
this_thread::sleep_for(chrono::milliseconds(id(dre)));
cout.put(c).flush();
}
}
catch(const std::exception& e)
{
cerr << e.what() << '\n';
cerr << this_thread::get_id()<<endl;
}
}
int main(){
try{
thread t1(doSomething,5,'.');
cout<<"start thread"<<t1.get_id()<<endl;
//启动了多个异步线程
for(int i=0;i<5;++i){
thread t(doSomething,10,'a'+i);//启动了5个线程
cout<<"detach start thread"<<t.get_id()<<endl;
t.detach();
}
cin.get();
cout<<"join thread"<<t1.get_id()<<endl;
//进行线程同步
t1.join();
}
catch(const exception& e){
cerr<<e.what()<<endl;
}
}
- 卸离之后,无法控制线程。最好使用值传递的方式启动线程。使用引用传递在线程中可能会访问无效的变量(已经被销毁)。
promise说明
提供了异步通信的方法。async相当于自动设置了promise推端,利用return语句抛出一个promise,解锁future.get的阻塞;使用thread启动线程的话,需要手动设置promise实现信号发出,接触future.get的阻塞。
- 类模板promise 提供存储值或异常的设施,之后通过promise 对象所创建的 future 对象异步获得结果。promise 只应当使用一次。
- 每个 promise 与共享状态关联,共享状态含有一些状态信息和可能仍未求值的结果,它求值为值(可能为 void )或求值为异常。 promise 可以对共享状态做三件事:
- 使就绪: promise 存储结果或异常于共享状态。标记共享状态为就绪,并解除阻塞任何等待于与该共享状态关联的 future 上的线程。
- 释放: promise 放弃其对共享状态的引用。若这是最后一个这种引用,则销毁共享状态。除非这是 std::async 所创建的未就绪的共享状态,否则此操作不阻塞。
- 抛弃: promise 存储以 future_errc::broken_promise 为 error_code 的 future_error 类型异常,令共享状态为就绪,然后释放它。
- promise 是 promise-future 交流通道的“推”端:存储值于共享状态的操作同步于任何在共享状态上等待的函数(如 std::future::get )的成功返回。其他情况下对共享状态的共时访问可能冲突:例如shared_future::get 的多个调用方必须全都是只读,或提供外部同步。
函数 | 作用 |
---|
get_future | 返回与承诺的结果关联的 future(公开成员函数) |
set_value | 设置结果为指定值(公开成员函数) |
set_value_at_thread_exit | 设置结果为指定值,同时仅在线程退出时分发提醒(公开成员函数) |
set_exception | 设置结果为指示异常(公开成员函数) |
set_exception_at_thread_exit | 设置结果为指示异常,同时仅在线程退出时分发提醒(公开成员函数) |
promise编程
#include<thread>
#include<future>
#include<iostream>
#include<string>
#include<exception>
#include<functional>
#include<utility>
using namespace std;
void doSomething(promise<string>& p){
try{
cout<<"read char x for exception"<<endl;
char c = cin.get();
if(c=='x'){
throw runtime_error(string("char")+c+"fault");
}
else{
string s = string("char")+c+"correct";
p.set_value(move(s));//移动赋值函数。防止退出局部变量后销毁。
}
}
catch(...){
p.set_exception(current_exception());
}
}
int main()
{
try{
promise<string>p;
thread t(doSomething,std::ref(p));
t.detach();
future<string> f(p.get_future());
cout<<"result:"<<f.get()<<endl;
}
catch(const exception& e){
std::cerr<<"exception"<<e.what()<<endl;
}
}
package_task说明
- 是一个线程池,可以用来多次启动某一个线程。
- 是async方法的扩展版,通过return语句来抛出一个默认的promise,解锁future的执行。
函数 | 作用 |
---|
get_future | 返回与承诺的结果关联的 std::future |
operator() | 执行函数 |
make_ready_at_thread_exit | 执行函数,并确保结果仅在一旦当前线程退出时就绪 |
reset | 重置状态,抛弃任何先前执行的存储结果 |
package_task编程说明
//线程执行的函数
double doSomething(int x,int y);
//申请一个线程池。
package_task<double(int,int)> task(doSomething);
//获得线程池的future
future<double> f = task.get_future();
//使用线程池启动一个县城
task(7,5);
//使用future获得线程执行的结果。
double res = f.get();
2 基于多路复用的并发
事件驱动的IO主要通过第三方库实现包括以下几种
- asio
- C++语言跨平台。用bind做回调也并不比虚函数好,看上去灵活了,代价却更高了。不光是运行时的内存和时间代价,编译时间也更长。基于ASIO开发应用,要求程序员熟悉函数对象,函数指针,熟悉boost库中的boost::bind,内存管理控制方面。
- asio是一个高性能的网络开发库,Windows下使用IOCP,Linux下使用epoll
- ACE
- ACE太过庞大,使得你即便是只使用它的一小部分,也不得不引用它的全部。而且框架一大堆,模式一个加一个,很多编程习惯也要改变。学习曲线太陡,也难以将它作为一个模块集成自己的应用。
- apr
- 大约只是一个平台无关的api封装,相对来说比较轻量级。
- muduo
- 这是一个用纯c++写的库,仅在linux下使用,one loop per thread的思想贯穿其中,将I/O 定时 信号都通过文件描述符的方式融合在一起,三类事件等同于一类事件来看待。仅在linux下使用。
- Libevent、libev、libuv
- 三个网络库,都是C语言实现的异步事件库(Asynchronousevent library),异步事件通知机制就是根据发生的事件,调用相应的回调函数进行处理。
- libevent :名气最大,应用最广泛,历史悠久的跨平台事件库;libev :较libevent而言,设计更简练,性能更好,但对Windows支持不够好;libuv :开发node的过程中需要一个跨平台的事件库,他们首选了libev,但又要支持Windows,故重新封装了一套,linux下用libev实现,Windows下用IOCP实现;
- libuv 可以说是 C 语言的异步库所能达到的最高高度了。完完全全的触碰到了C语言的自身瓶颈,好在 libuv 只是 nodejs 的底层库,上层软件转移到 javascript 语言而逃避了 C 的禁锢。
特性 | libevent | libev | libuv |
---|
优先级 | 激活的事件组织在优先级队列中,各类事 件默认的优先级是相同的,可以通过设置 事件的优先级使其优先被处理 | 也是通过优先级队列来管理激活的时间, 也可以设置事件优先级 | 没有优先级概念,按照固定的顺序访 问各类事件 |
事件循环 | event_base用于管理事件 | 激活的事件组织在优先级队列中,各类事件默认的优先级是相同的, 可以通 过设置事件的优先级 使其优先被处理 | |
线程安全 | event_base和loop都不是线程安全的,一个event_base或loop实例只能在用户的一个线程内访问(一般是主线程),注册到event_base或者loop的event都是串行访问的,即每个执行过程中,会按照优先级顺序访问已经激活的事件,执行其回调函数。所以在仅使用一个event_base或loop的情况下,回调函数的执行不存在并行关系 | | |
如果要学习的话asio和libuv感觉还不错。libuv可以实现跨平台的高性能通信。