一种简单的用户态Python多任务(线程)方案
在Python界,我们常常使用多进程来实现并行加速。在各进程内部,再采用多线程方案进行io调度,但这通常有一些问题,我们这里以传统的CPython实现为准,该实现直接采用来操作系统原生线程实现多线程,这有一些问题,首先,操作系统线程并不是免费的,它有自己的栈、内核数据结构,并且(通常)是基本调度单元,这会带来一些并不算不重要的overhead,我们在这种场景下使用多线程的目的本质上就是进行io调度,那么大部分的抢占对我们是没有意义的;第二,Python实现中,每个线程执行一定条数的字节码后主动让出时间片,这种调度时机并不是我们想要的。对于特定的应用,这可以用IO多路复用来解决,IO多路复用就是一个IO调度器,但是,这种方式不能提供一种透明的抽象,进行IO的控制流必须显式得处理回调、事件等等,另外,我们能够同时进行的控制流(任务)受到了不必要的限制——它本质上应该受限于我们处理IO的能力。
4.1 对控制流的抽象
我们用一个"闭包中的callable对象"来表达控制流中的某一状态,我们可以得到这个对象,就得到了当前控制流,通过对该对象的调用恢复执行控制流。大体上是这样:
def task(arg):
def task_child(a):
return some_func(arg, a)
return task_child
对于一般的控制流的抽象,我们应该了解continuation,参考:
http://en.易做图.org/wiki/Continuation
4.2 设计
4.2.1task
我们把满足下属条件的Python对象叫做task:
可以以某种确定的方式被调用,有0到多个入参
对该task的调用,应该返回
Complete,结果
Failed
一个list,该list由形如(head,tail1,tail2…)的元素组成,我们把这种元素叫做task元祖
我们把满足下述条件的tuple叫做task元祖: 形如:(head, tail1, tail2…)
head为一个task,且接受该tuple长度减1个参数
tail1、tail2应该是:
一个Python对象
一个task元祖
IO task
一个拥有触发条件的task叫做IO task.IO task只在满足触发条件的情况下才会被调度。
4.3 实现
调度器维护一个task元组的队列,根据特定的调度策略选择task执行,执行的结果将被投入该队列。IO task在满足触发条件的时候被调度,一种典型的IO task:
def recvsegment(s, length):
rcv = s.recv(10000000)
if len(rcv) == length:
return rcv
return lambda:rcv + recvsegment(s, length-len(rcv))
我们在每次调度的时候,会检查与一个IO task联系的标识符(文件描述符)是否准备好,如果准备好,就执行该IO task.对于task元组中参数被求值完毕(所有的tail都为一个值)的情况,我们会以这些参数为入参,执行head。在这种设计下,任何IO操作应该和主控制流分离,以(head, io1,io2…)的方式进行。
事实上,task的执行载体可以是原生线程,这样,就实现了M:N的并发模型,对于特殊的情况(IO完成却没有机会被调度倒的task),还可以动态增减线程进行处理。
在特定的假设下,这套多任务方案有一定的意义。这个假设就是:1.IO占据了控制流的大部分时间。2.任何一种非IO操作都可以很快完成。因此,在特殊的时机交出控制权,而不是抢占调度,是可以接受的。事实上,现实世界有很多系统满足这样的要求。
补充:Web开发 , Python ,