一种简单的用户态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.wikipedia.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操作都可以很快完成。因此,在特殊的时机交出控制权,而不是抢占调度,是可以接受的。事实上,现实世界有很多系统满足这样的要求。

相关内容

    暂无相关文章

评论关闭