程序的运行模型应该有很多种,最简单的是串行程序:程序从开始就一直运行到结束,中间没有等待和休息,和其他程序也没有关系;并行程序:多个同时运行的串行程序彼此之间有互斥和同步的关系,互斥大家都懂,通过锁和等待来实现,同步应该算是一种协作关系,比如一个程序给另一个程序发消息请求做某件事,必须要等待收到回应消息后才能继续执行;简单的网络服务程序中,根据从socket收到的不同请求来分支执行不同的程序,图形界面程序中,提供给按钮对象一个回调函数,当鼠标点击按钮时,图形系统就会执行用户的这个函数;硬件中断和中断处理函数;等等。
这些各种各样的运行模式大多数都可以用事件驱动的模型来概括,程序事先安排好针对各种事件的处理代码或函数,然后等待事件的到来,根据到来事件的类型执行相应的代码或函数,个人理解事情驱动中事件和处理代码必须是预先都确定的,静态的,如果程序能够在运行时动态识别未知事件,动态安排和组织代码,那就是有智能了。这里暂时把处理一种事件的代码或函数叫做一个任务。
事件驱动模型的具体实现方式有很多种。比如等待事件的方式就有很多种:最简单的是一个事件一个事件的轮询(忙等待),现在比较流行的是利用操作系统专门提供的等待函数(可以同时等待多个事件,并且不占用CPU),比如Window下的完成端口,Linux下的select和epoll等;事件触发的任务怎么运行又有很多种:有的是每个任务都单独创建一个线程来执行,有的是把待运行的任务放到一个链表上,有若干个执行线程;也有的完全没有执行线程,就是一个主线程先等待事件发生、依次执行完所有任务后再等待事件,这样一个大循环。
操作系统提供的是进程和线程的概念,任务应该是一种比线程更细的概念,一个任务内部是串行的,多个任务之前可能有同步和互斥的关系。进程和线程就像是做事情的人,而任务就是具体的事情,有以下类似的关系:
1、一个线程同时只能运行一个任务,可以依次运行多个任务;一个人同时只能做一件事情,可以依次做多件事情
2、一个任务应该不能跨线程运行;一件事情应该自始至终由一个人完成。
3、线程和任务的搭配可以很灵活,可以每个任务都安排一个线程执行,也可能一个线程只执行一种任务,甚至可能一个线程做所有的任务;一件事情一个人做,一种事情一个人做,一个人做所有的事情。
4、线程的个数受资源的影响有限制,不能无限多,比如每个线程都要占用一定内存,单处理上的线程切换等;做事情的人当然也不能无限多,总要吃饭睡觉吧。
5、多个任务之间有互斥的关系,如果一些任务运行的大部分时间都要占用同一个互斥资源,那么安排多个线程同时运行这些任务和安排一个线程轮流运行这些任务是差不多的。事情也一样,互斥资源就是做事情的工具。
6、多个任务之间有同步的关系,一个任务可能触发另一个任务运行,一个任务可能要等待另一个任务运行完才能继续运行;做事情也一样。
基于上面的想法,能否在应用层设计一种任务运行系统,满足一下要求:
1、能够在一个线程中完成事件等待和所有任务的运行。
2、只要目前有事件发生或有任务可以运行,这个线程就不能等待。
3、允许任务之间有同步的关系或者任务需要等待外界的条件。
如果没有第3条,那么这个目标不难到达(例如libevent)。
我相信如果能够只用一个线程完成所有的任务,那么这个模型就能很容易的扩展到多个线程,并且有助于我们理解和改进其他人设计的运行模型。
举一个实际的人和事情的例子:医生看病。假定在医生看病的流程中有两个任务:
1、看病:听诊 -> 等待验血结果 -> 开药
2、验血
看病的门口只要有病人排队,就触发医生看病,医生看病触发去验血,收到验血结果后开药。验血的门口只要有病人排队,就触发医生验血。
我们先看一种简单情况:安排两个医生,一个只看病,一个只验血(即两个线程来执行这两个任务),看病流程中的等待验血结果会阻塞看病线程的执行,导致不能看后面的病人,这肯定是不行的。有人提出来一个办法:把看病任务拆成两个任务:
1、看病1:听诊
2、看病2:开药
保证每个任务执行中都是没有等待的,然后验完血触发看病2任务,不需要增加医生,可能需要增加一个等待开药的队列。看病的医生可以接待听诊和开药队列上的病人执行相应的任务。这个方法就是把一个同步的任务拆成几个异步的子任务,在这个例子中当然是可行的,但是有以下几个缺点:
1、增加了程序设计的难度,把一个简单易懂的串行流程任务拆成若干个子任务,还要协调这几个子任务运行,如果流程中等待的地方比较多,就更麻烦了。
2、有时候不可行,因为一个复杂的程序流程中可能有很多分支,有的分支不需要等待,有的分支又需要等待。
不能再安排看病的医生了。
实际生活中我们是怎么解决这个问题的呢?其实很简单:看病的医生在等待验血结果时仍然听诊下一个病人,只是在听诊完一个病人时就看一下有没有验血的病人回来,如果回来了就给他开药。在程序里怎么实现这个过程呢,其实就是在看病这个函数中,等待验血结果时并不真正的阻塞线程的执行,而是进入事件等待和分发的过程,这样如果后面还有病人,就会再次重入调用看病函数。假如有3个病人,那么病人1的看病任务被中断,嵌套病人2的看病任务,再嵌套病人3的看病任务,因为没有病人了,所以医生等待病人3的验血结果,开药,然后返回病人2的看病任务,开药,然后返回病人1的看病任务,开药。这个过程完全由函数调用的压栈、出栈模拟,因为栈只能先进后出,所以即使病人1的验血结果先出来,也必须最后开药,这个不公平还是可以接受的。
如果看病的人太多,那么函数调用的堆栈还是可能溢出的,可以通过限制同一个任务嵌套的层次数来解决:如果累计了10个验血结果的病人,就不再调用看病任务了,只是等待。这样应该就比较完美的解决了两个线程(医生)的看病问题。
明天有时间再写一个线程的问题