asio 库基于操作系统提供的异步机制,采用前摄器设计模式(Proactor)实现了可移植的异步(或者同步)I/O操作,而且并不要求使用多线程和锁,有效地避免了多线程编程带来的诸多有害副作用(如条件竞争、死锁等)。
同步模式:
在同步模式下,程序发起一个I/o操作,向io_service提交请求,io_service把操乍转交给操作系统,同步地等待。当I/o操作完成时,操作系统通知io_service,然后io_service 再把结果发回给程序,完成整个同步流程。这个处理流程与多线程的join ()等待方式很相似。
异步模式
在异步模式下,程序除了要发起的I/0操作外,还要定义一个用于回调的完成处理函数( complete handler)。io_service同样把I/0操作转交给操作系统执行,但它不同步等待,而是立即返回。调用io_service的run ()成员函数可以等待异步操作完成,当异步操作完成时io_service 从操作系统获取结果,再调用handler执行后续的逻辑。
handler是asio库里的重要概念,它是符合某种函数签名的回调函数。handler必须是可拷贝的, io_service 会存储handler 的拷贝,当某种异步事件发生时io_service就会调用事件对应的handler。
asio中主要三种handle
void handler(const error_code&ec)//操作错误码
void handler(const error_code&ec,int signal_number)//引发操作的信号
void handler(const error_code&ec,std::size_t bytes_trransferred)//传输的字节
-
- class io_service:private noncopyable
- {
- public:
- std::size_t run(); //阻塞事件循环
- std::size_t run_one(); //阻塞至少执行一个handler
-
- std::size_t poll(); //非阻塞 执行一个ready的handler
- std::size_t poll_one(); //至少执行一个ready的一个handler
-
- void stop(); //停止事件循环
- bool stopped() const;
- bool reset() const;
-
- unspecified dispatch(Handler handler);//要求异步执行一个handler
- unspecified post(Handler handler);
-
- class strand;//内部线程
- class work;//表示有工作在进行
-
-
- }
io service类代表了系统里的异步处理机制(如epoll),必须在asio库里的其他对象之前初始化,其他对象则向io_service提交异步操作的handler。
asio库基于操作系统的异步I/0模型,不直接使用系统线程,而是定义了一个自己的线程概念: strand,它序列化异步操作,保证异步代码在多线程的环境中可以正确地执行,无需使用互斥量。
- class io_service::strand{
- public:
- explicit strand(io_serbice&io);
- io_service&get_io_service();
- unspecified dispatch(Handler handler);//要求异步执行一个handler
- unspecified post(Handler handler);// 要求异步执行一个handler
- unspecified wrap(Handler handler);//要求包装一个 handler
-
- bool running_in_this_thread() const;
-
- }
strand常用的成员函数是wrap(),它可以包装一个函数,返回一个相同签名的函数对象,保证线程安全地在strand中执行。
我们可以把strand理解为对一组handler的锁,加入了线程保护,这一组里的handler不会存在线程并发访问的问题。
在一个线程里使用io_service是没有竞争的,本身就是“线程安全”,不需要strand。但如果多个线程对一个io_service对象执行run (),那么同一个handler就有可能存在线程竞争,需要使用strand保护。
当io_service里注册的所有事件完成时它就会退出事件循环,有的时候这可能不是我们所希望的,我们想让io_service仍然继续运行,处理将来可能发生的异步事件,这时就需要让io_service始终“有事可做”。
I/O操作经常会使用到数据缓存区,相当一片指定的内存区域
- class mutable_buffer
- {
- public:
- mutable_buffer();
- mutable_buffer(void* data,std::size_t size);
-
- private:
- void* data_;
- std::size_t size_;
-
- };
-
- class const_buffer
- {
- public:
- const_buffer();
- const_buffer(void* data,std::size_t size);
- const_buffer(const mutable_buffer&b);
- private:
- void* data_;
- std::size_t size_;
- }
- //
- buffer(std::vector&data);
- buffer(void* data,std::size_t size_in_bytes);
-
-
-
- //自由操作函数
- buffer_size() :获取长度
- buffer_cast
() :转换指针类型 - buffer_copy() :拷贝缓冲区类型
··
我们通常使用工厂函数 buffer()来产生 buffer对象,它能够包装常用的c++容器类型,如原始数组、array、vector、string 等,返回 mutable_buffers_1或const_buffers_l对象。
asio库使用system库的error_code和 system_error来表示程序运行的错误。基本上所有的异步操作函数都有两种重载形式:
一种形式是有一个error_code&的输出参数,调用后可以检查这个参数验证是否发生了错误,或者简单地忽略;
另一种形式没有error_code 参数,如果发生了错误会抛出 system_error 异常,调用代码必须使用try-catch块来捕获错误,无法忽略。
异步代码的执行是非线性的,运行时的流程很难调试。asio库为此特别提供了handle跟踪机制,只要在头文件
跟踪日志以"|“分为四个字段 格式:
tag|timestamp|action|description
第一个字段是标记字符串,目前总是“@asio”。第二个字段是UNIx时间戳,精确到毫秒分辨率。最后两个字段很重要,它们记录了异步代码的具体动作,含义是:
n | n号handler正执行某些操作 |
>n | 进入n号handler,descriptio表示入口函数 |
离开n号handler 无description | |
!n | 发生异常 离开n号handler |
~n | n号handler未被调用就被销毁 |
n*m | n号handler创建一个新的m号handler |
asio还提供了一个脚本 handlerviz.pl,它位于 Boost 源码包的“libs/asio/tools/”目录,可以把日志可视化,生成png 或pdf 文件,更清晰地看出异步操作的调用顺序。
Reactor是这样一种模式,它要求主线程(IO处理单元,下同)只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元,下同)。除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
分析:
①主线程往epoll内核事件表中注册socket上有数据可读
②主线程调用epoll_wait等待socket上有数据可读
③当socket上有数据可读时,epoll_wait通知主线程。主线程则将socket可读事件放入请求队列
④睡眠在请求请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪时间
⑤主线程调用epoll_wait等到socket可写
⑥当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列
⑦睡眠在请求队列上的某个工作线程被唤醒,它向socket上写入服务器处理客户请求的结果
与Reactor不同,Proactor模式将所有的I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑
1)主线程调用aio_read函数向内核注册socket 上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(可以看成一个服务端)
2)主线程继续处理其他逻辑
3)当socket 上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。
4)应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序(仍然以信号为例)。
(5)主线程继续处理其他逻辑。
(6))当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
在上图中,连接socket上的读写事件是通过aio_read/aio_write向内核注册的,因此内核将通过信号来向应用程序报告连接socket上的读写事件。所以,主线程的epoll_wait调用仅能用来检测监听socket上的连接请求事件,而不能用来检测连接socket的读写事件
原理:
主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一“完成事件”。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理
工作流程:
①主线程往epoll内核事件表中注册socket上的读就绪事件
②主线程调用epoll_wait等待socket上有数据可读
③当socket上有数据可读时,epoll_wait通知主线程。主线程从socket循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列
④睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件
⑤主线程调用epoll_wait等到socket可写
⑥当socket可写时,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果