
一、背景
在Reactor和Proactor模型一文中讲到,Reactor模型提供了一个比较理想的I/O编程框架,让程序更有结构,用户使用起来更加方便,比裸API调用开发效率要高。另外一方面,如果希望每个事件通知之后,做的事情能有机会被代理到某个线程里面去单独运行,而线程完成的状态又能通知回主任务,那么“异步”的机制就必须被引入。本文以boost.Asio库(其设计模式为Proactor)为基础,讲解为什么需要异步编程以及异步编程的实现。
二、举例
跑步
设想你是一位体育老师,需要测验100位同学的400米成绩。你当然不会让100位同学一起起跑,因为当同学们返回终点时,你根本来不及掐表记录各位同学的成绩。
如果你每次让一位同学起跑并等待他回到终点你记下成绩后再让下一位起跑,直到所有同学都跑完。恭喜你,你已经掌握了同步阻塞模式。你设计了一个函数,传入参数是学生号和起跑时间,返回值是到达终点的时间。你调用该函数100次,就能完成这次测验任务。这个函数是同步的,因为只要你调用它,就能得到结果;这个函数也是阻塞的,因为你一旦调用它,就必须等待,直到它给你结果,不能去干其他事情。
如果你一边每隔10秒让一位同学起跑,直到所有同学出发完毕;另一边每有一个同学回到终点就记录成绩,直到所有同学都跑完。恭喜你,你已经掌握了异步非阻塞模式。你设计了两个函数,其中一个函数记录起跑时间和学生号,该函数你会主动调用100次;另一个函数记录到达时间和学生号,该函数是一个事件驱动的callback函数,当有同学到达终点时,你会被动调用。你主动调用的函数是异步的,因为你调用它,它并不会告诉你结果;这个函数也是非阻塞的,因为你一旦调用它,它就马上返回,你不用等待就可以再次调用它。但仅仅将这个函数调用100次,你并没有完成你的测验任务,你还需要被动等待调用另一个函数100次。
当然,你马上就会意识到,同步阻塞模式的效率明显低于异步非阻塞模式。那么,谁还会使用同步阻塞模式呢?不错,异步模式效率高,但更麻烦,你一边要记录起跑同学的数据,一边要记录到达同学的数据,而且同学们回到终点的次序与起跑的次序并不相同,所以你还要不停地在你的成绩册上查找学生号。忙乱之中你往往会张冠李戴。你可能会想出更聪明的办法:你带了很多块秒表,让同学们分组互相测验。恭喜你!你已经掌握了多线程同步模式!
每个拿秒表的同学都可以独立调用你的同步函数,这样既不容易出错,效率也大大提高,只要秒表足够多,同步的效率也能达到甚至超过异步。
可以理解,你现的问题可能是:既然多线程同步既快又好,异步模式还有存在的必要吗?
很遗憾,异步模式依然非常重要,因为在很多情况下,你拿不出很多秒表。你需要通信的对端系统可能只允许你建立一个SOCKET连接,很多金融、电信行业的大型业务系统都如此要求。
三、背景知识介绍
3.1 同步函数VS异步函数
以下部分主要来自于:https://www.cnblogs.com/balingybj/p/4780442.html
依据微软的MSDN上的解说:
(1)、同步函数:当一个函数是同步执行时,那么当该函数被调用时不会立即返回,直到该函数所要做的事情全都做完了才返回。
(2)、异步函数:如果一个异步函数被调用时,该函数会立即返回尽管该函数规定的 *** 作任务还没有完成。
(3)、在一个线程中分别调用上述两种函数会对调用线程有何影响呢?
当一个线程调用一个同步函数时(例如:该函数用于完成写文件任务),如果该函数没有立即完成规定的 *** 作,则该 *** 作会导致该调用线程的挂起(将CPU的使用权交给系统,让系统分配给其他线程使用),直到该同步函数规定的 *** 作完成才返回,最终才能导致该调用线程被重新调度。
当一个线程调用的是一个异步函数(例如:该函数用于完成写文件任务),该函数会立即返回尽管其规定的任务还没有完成,这样线程就会执行异步函数的下一条语句,而不会被挂起。那么该异步函数所规定的工作是如何被完成的呢?当然是通过另外一个线程完成的了啊;那么新的线程是哪里来的呢?可能是在异步函数中新创建的一个线程也可能是系统中已经准备好的线程。
(4)、一个调用了异步函数的线程如何与异步函数的执行结果同步呢?
为了解决该问题,调用线程需要使用“等待函数”来确定该异步函数何时完成了规定的任务。因此在线程调用异步函数之后立即调用一个“等待函数”挂起调用线程,一直等到异步函数执行完其所有的 *** 作之后,再执行线程中的下一条指令。
我们是否已经发现了一个有趣的地方呢?!就是我们可以使用等待函数将一个异步执行的函数封装成一个同步函数。
3.2 同步调用VS异步调用
*** 作系统发展到今天已经十分精巧,线程就是其中一个杰作。 *** 作系统把 CPU 处理时间划分成许多短暂时间片,在时间 T1 执行一个线程的指令,到时间 T2 又执行下一线程的指令,各线程轮流执行,结果好象是所有线程在并肩前进。这样,编程时可以创建多个线程,在同一期间执行,各线程可以“并行”完成不同的任务。
在单线程方式下,计算机是一台严格意义上的冯·诺依曼式机器,一段代码调用另一段代码时,只能采用同步调用,必须等待这段代码执行完返回结果后,调用方才能继续往下执行。有了多线程的支持,可以采用异步调用,调用方和被调方可以属于两个不同的线程,调用方启动被调方线程后,不等对方返回结果就继续执行后续代码。被调方执行完毕后,通过某种手段通知调用方:结果已经出来,请酌情处理。
计算机中有些处理比较耗时。调用这种处理代码时,调用方如果站在那里苦苦等待,会严重影响程序性能。例如,某个程序启动后如果需要打开文件读出其中的数据,再根据这些数据进行一系列初始化处理,程序主窗口将迟迟不能显示,让用户感到这个程序怎么等半天也不出来,太差劲了。借助异步调用可以把问题轻松化解:把整个初始化处理放进一个单独线程,主线程启动此线程后接着往下走,让主窗口瞬间显示出来。等用户盯着窗口犯呆时,初始化处理就在背后悄悄完成了。程序开始稳定运行以后,还可以继续使用这种技巧改善人机交互的瞬时反应。用户点击鼠标时,所激发的 *** 作如果较费时,再点击鼠标将不会立即反应,整个程序显得很沉重。借助异步调用处理费时的 *** 作,让主线程随时恭候下一条消息,用户点击鼠标时感到轻松快捷,肯定会对软件产生好感。
异步调用用来处理从外部输入的数据特别有效。假如计算机需要从一台低速设备索取数据,然后是一段冗长的数据处理过程,采用同步调用显然很不合算:计算机先向外部设备发出请求,然后等待数据输入;而外部设备向计算机发送数据后,也要等待计算机完成数据处理后再发出下一条数据请求。双方都有一段等待期,拉长了整个处理过程。其实,计算机可以在处理数据之前先发出下一条数据请求,然后立即去处理数据。如果数据处理比数据采集快,要等待的只有计算机,外部设备可以连续不停地采集数据。如果计算机同时连接多台输入设备,可以轮流向各台设备发出数据请求,并随时处理每台设备发来的数据,整个系统可以保持连续高速运转。编程的关键是把数据索取代码和数据处理代码分别归属两个不同的线程。数据处理代码调用一个数据请求异步函数,然后径自处理手头的数据。待下一组数据到来后,数据处理线程将收到通知,结束 wait 状态,发出下一条数据请求,然后继续处理数据。
异步调用时,调用方不等被调方返回结果就转身离去,因此必须有一种机制让被调方有了结果时能通知调用方。在同一进程中有很多手段可以利用,笔者常用的手段是回调、event 对象和消息。
回调:回调方式很简单:调用异步函数时在参数中放入一个函数地址,异步函数保存此地址,待有了结果后回调此函数便可以向调用方发出通知。如果把异步函数包装进一个对象中,可以用事件取代回调函数地址,通过事件处理例程向调用方发通知。
event : event 是 Windows 系统提供的一个常用同步对象,以在异步处理中对齐不同线程之间的步点。如果调用方暂时无事可做,可以调用 wait 函数等在那里,此时 event 处于 nonsignaled 状态。当被调方出来结果之后,把 event 对象置于 signaled 状态,wait 函数便自动结束等待,使调用方重新动作起来,从被调方取出处理结果。这种方式比回调方式要复杂一些,速度也相对较慢,但有很大的灵活性,可以搞出很多花样以适应比较复杂的处理系统。
消息:借助 Windows 消息发通知是个不错的选择,既简单又安全。程序中定义一个用户消息,并由调用方准备好消息处理例程。被调方出来结果之后立即向调用方发送此消息,并通过 WParam 和 LParam 这两个参数传送结果。消息总是与窗口 handle 关联,因此调用方必须借助一个窗口才能接收消息,这是其不方便之处。另外,通过消息联络会影响速度,需要高速处理时回调方式更有优势。
如果调用方和被调方分属两个不同的进程,由于内存空间的隔阂,一般是采用 Windows 消息发通知比较简单可靠,被调方可以借助消息本身向调用方传送数据。event 对象也可以通过名称在不同进程间共享,但只能发通知,本身无法传送数据,需要借助 Windows 消息和 FileMapping 等内存共享手段或借助 MailSlot 和 Pipe 等通信手段。
如果你的服务端的客户端数量多,你的服务端就采用异步的,但是你的客户端可以用同步的,客户端一般功能比较单一,收到数据后才能执行下面的工作,所以弄成同步的在那等。
几天前在博问中看到一个C# Socket问题 就想到笔者 年做的一个省级交通流量接收服务器项目 当时的基本求如下
接收自动观测设备通过无线网卡 Internet和Socket上报的交通量数据包 全年 * 运行的自动观测设备 分钟上报一次观测数据 每笔记录约 K大小 规划全省将有 个左右的自动观测设备(截止 年 月还只有 个) 当时 VS 才发布年多 笔者也是接触C#不久 于是Google了国内国外网 希望找点应用C#解决Socket通信问题的思路和代码 最后 找到了两篇帮助最大的文章 一篇是国人写的Socket接收器框架 应用了独立的客户端Socket会话(Session)概念 给笔者提供了一个接收服务器的总体框架思路 另一篇是美国人写的 提出了多线程 分段接收数据包的技术方案 描述了多线程 异步Socket的许多实现细节 该文坚定了笔者采用多线程和异步方式处理Socket接收器的技术路线
具体实现和测试时笔者还发现 在Internet环境下的Socket应用中 需要系统有极强的容错能力 没有办法控制异常 就必须允许它们存在(附加源代码中可以看到 try{}catch{}语句较多) 对此 笔者设计了一个专门的检查和清理线程 完成无效或超时会话的清除和资源释放工作
依稀记得 国内框架作者的名称空间有ibm 认为是IBM公司职员 通过邮件后才知道其人在深圳 笔者向他请教了几个问题 相互探讨了几个技术关键点 可惜 现在再去找 已经查不到原文和邮件了 只好借此机会 将本文献给这两个素未谋面的技术高人和同行 也盼望拙文或源码能给读者一点有用的启发和帮助
主要技术思路
整个系统由三个核心线程组成 并由 NET线程池统一管理
侦听客户端连接请求线程 ListenClientRequest() 循环侦听客户端连接请求 如果有 检测该客户端IP 看是否是同一观测设备 然后建立一个客户端TSession对象 并通过Socket异步调用方法BeginReceive()接收数据包 EndReceive()处理数据包 数据包处理线程 HandleDatagrams() 循环检测数据包队列_datagramQueue 完成数据包解析 判断类型 存储等工作 客户端状态检测线程 CheckClientState() 循环检查客户端会话表_sessionTable 判断会话对象是否有效 设置超时会话关闭标志 清楚无效会话对象及释放其资源 主要类简介
系统主要由 个类组成
TDatagramReceiver(数据包接收服务器) 系统的核心进程类 建立Socket连接 处理与存储数据包 清理系统资源 该类提供全部的public属性和方法 TSession(客户端会话) 由每个客户端的Socket对象组成 有自己的数据缓冲区 清理线程根据该对象的最近会话时间判断是否超时 TDatagram(数据包类) 判断数据包类别 解析数据包
关键函数和代码
下面简介核心类TDatagramReceiver的关键实现代码
系统启动
系统启动方法StartReceiver()首先清理资源 创建数据库连接 初始化若干计数值
然后创建服务器端侦听Socket对象 最后调用静态方法ThreadPool QueueUserWorkItem()在线程池中创建 个核心处理线程
Code/// /// 启动接收器/// public bool StartReceiver(){ try { _stopReceiver = true
this Close()
if (!this ConnectDatabase()) return false
_clientCount = _datagramQueueCount = _datagramCount = _errorDatagramCount = _exceptionCount =
lishixinzhi/Article/program/net/201311/12226
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)