Python多任务学习之 分别通过yield关键字、greenlet以及gevent实现多任务

  1. 使用yield关键字实现多任务

  前面,我们已经学习了分别通过线程和进程来完成多任务,实际上,使用Python中的yield关键字及表达式、生成器、生成器迭代器、生成器表达式详解中提到的yield关键字也可以实现多任务。如下述代码:

  import time

  def walk():

  while True:

  print("Steve Jobs was walking around the auditorium...")

  time.sleep(0.5)

  yield

  def speak():

  while True:

  print("Steve Jobs was introducing his new iPhone...")

  time.sleep(0.5)

  yield

  def main():

  task1 = walk()

  task2 = speak()

  while True:

  next(task1)

  next(task2)

  if __name__ == "__main__":

  main()

  上述代码的运行结果为:

  Steve Jobs was walking around the auditorium…

  Steve Jobs was introducing his new iPhone…

  Steve Jobs was walking around the auditorium…

  Steve Jobs was introducing his new iPhone…

  Steve Jobs was walking around the auditorium…

  Steve Jobs was introducing his new iPhone…

  Steve Jobs was walking around the auditorium…

  Steve Jobs was introducing his new iPhone…

  实际上,如果在函数walk和函数speak中没有关键字yield,则程序运行至第19行就会进入函数walk()中的死循环。上述代码中,yield关键字的存在使得两个函数都变成了生成器函数,结合Python中的yield关键字及表达式、生成器、生成器迭代器、生成器表达式详解中对该关键字的介绍,在main()函数中循环对两个生成器迭代器使用next()函数即可实现上述效果。

  2. greenlet简介及使用greenlet实现多任务

  greenlet是Python中基于yield关键字实现的用于实现多任务的模块,下面我们将看到,正是因为这一点,所以使用greenlet模块实现多任务的现象和使用yield关键字非常相似。

  2.1 greenlet简介

  “greenlet”是一个轻量级的任务执行单元,用于实现多任务间按时序协作。使用“greenlet”实现多任务需要导入同名第三方模块。

  greenlet的官方文档对其简介为:

  A “greenlet” is a small independent pseudo-thread.

  一个“greenlet”是一个独立的伪线程。

  Think about it as a small stack of frames; the outermost (bottom) frame is the initial function you called, and the innermost frame is the one in which the greenlet is currently paused.

  可将一个“greenlet”视为一个栈结构,该栈存储了多个帧;最外(栈底)的帧是最先调用的函数,最内(栈顶)的帧是该“greenlet”当前暂停处的信息。

  You work with greenlets by creating a number of such stacks and jumping execution between them.

  你可以通过类似创建数个栈的方式来使用数个“greenlet”,并且在各个“greenlet”之间平行切换(即一个greenlet切换至另一个greenlet时,会切换至后者上次执行后挂起的位置)执行。

  Jumps are never implicit: a greenlet must choose to jump to another greenlet, which will cause the former to suspend and the latter to resume where it was suspended.

  多个“greenlet”之间的切换一定都是显式的:必须由一个“greenlet”选择切换到另外一个“greenlet”,这会导致前者暂停而后者从上次暂停处恢复执行。

  Jumping between greenlets is called “switching”.

  在不同“greenlet”之间切换的操作被称为“switching(切换)”。

  When you create a greenlet, it gets an initially empty stack; when you first switch to it, it starts to run a specified function, which may call other functions, switch out of the greenlet, etc.

  当创建一个“greenlet”,其即获得一个空栈;当程序第一次“切换”至该“greenlet”,其启动执行指定的函数,该函数又可以调用其他函数或者切换出该“greenlet”等。

  When eventually the outermost function finishes its execution, the greenlet’s stack becomes empty again and the greenlet is “dead”.

  当栈底的函数最终完成执行,则该“greenlet”的栈又一次变为空,则该“greenlet”就“dead(死)”了。

  Greenlets can also die of an uncaught exception.

  “greenlet”还可能应未捕捉的异常而“死”。

  根据上述简介,结合下列示例代码:

  from greenlet import greenlet

  def test1():

  print(12)

  gr2.switch()

  print(34)

  def test2():

  print(56)

  gr1.switch()

  print(78)

  gr1 = greenlet(test1)

  gr2 = greenlet(test2)

  gr1.switch()

  其运行结果为:

  12

  56

  34

  可分析,上述程序的运行过程应该是:

  最后一行执行后将切换至test1;

  test1打印数字12,然后切换至test2;

  打印数字56,切换回test1,打印数字34;

  test1执行完毕且gr1“死亡”。

  2.1.1 父greenlet

  Let’s see where execution goes when a greenlet dies.

  下面看一下当一个“greenlet”死亡后,程序应该前往哪里执行。

  Every greenlet has a “parent” greenlet.

  每一个“greenlet”都有一个父“greenlet”。

  The parent greenlet is initially the one in which the greenlet was created (this can be changed at any time).

  刚创建的“greenlet”,其父“greenlet”是初始创建它的那个。

  The parent is where execution continues when a greenlet dies.

  当一个“greenlet”死亡,程序将回到父“greenlet”继续执行。

  This way, greenlets are organized in a tree.

  因此,多个“greenlet”通过树状形式组织在一起。

  Top-level code that doesn’t run in a user-created greenlet runs in the implicit “main” greenlet, which is the root of the tree.

  不运行于用户创建的“greenlet”的上层代码,以隐式的方式运行于主“greenlet”中,这是树的根。

  In the above example, both gr1 and gr2 have the main greenlet as a parent.

  上述案例中,gr1和gr2的父“greenlet”都是主“greenlet”。

  Whenever one of them dies, the execution comes back to “main”.

  无论哪个“greenlet”死亡,程序都将回到主“greenlet”中(所以上述代码永远不会打印数字78)。

  Uncaught exceptions are propagated into the parent, too.

  未经捕获的异常也将被传递至父“greenlet”中。

  For example, if the above test2() contained a typo, it would generate a NameError that would kill gr2, and the exception would go back directly into “main”.

  比如,如果上述test2()中存在打字错误,这将产生一个NameError并杀死gr2,那么程序执行将直接回到主“greenlet”。

  Remember, switches are not calls, but transfer of execution between parallel “stack containers”.

  需要注意的是,“切换”并非调用,而只是在平行的“栈容器”间来回切换执行程序。

  2.1.2 greenlet实例化

  greenlet.greenlet is the greenlet type, which supports the following operations:

  greenlet包中有一个同名的greenlet类,其支持以下操作:

  greenlet(run=None, parent=None):

  Create a new greenlet object (without running it). run is the callable to invoke, and parent is the parent greenlet, which defaults to the current greenlet.

  创建一个“greenlet”对象(并没有运行它)。run是可调用对象,parent是父“greenlet”,默认为当前“greenlet”。

  greenlet.getcurrent():

  Returns the current greenlet (i.e. the one which called this function).

  返回当前“greenlet”(即调用该函数的“greenlet”)。

  greenlet.GreenletExit:

  This special exception does not propagate to the parent greenlet; it can be used to kill a single greenlet.

  该特殊异常不会传递至父“greenlet”,可用于杀死当前的“greenlet”。

  2.1.3 greenlet切换

  Switches between greenlets occur when the method switch() of a greenlet is called, in which case execution jumps to the greenlet whose switch() is called.

  不同“greenlet”之间的切换发生于调用一个“greenlet”的switch()方法时,此时程序切换至该“greenlet”内。

  During a switch, an object or an exception is “sent” to the target greenlet; this can be used as a convenient way to pass information between greenlets.

  在切换期间,一个对象或异常可以被“传递”至目标“greenlet”;这是一个在不同“greenlet”之间传递信息的方便方法。

  如下列代码所示:

  def test1(x, y):

  z = gr2.switch(x+y)

  print(z)

  def test2(u):

  print(u)

  gr1.switch(42)

  gr1 = greenlet(test1)

  gr2 = greenlet(test2)

  gr1.switch("hello", " world")

  上述代码的运行结果为:

  hello world

  42

  2.2 使用greenlet完成多任务

  from greenlet import greenlet

  import time

  def walk():

  while True:

  print("Steve Jobs was walking around the auditorium...")

  task2.switch()

  time.sleep(0.5)

  def speak():

  while True:

  print("Steve Jobs was introducing his new iPhone...")

  task1.switch()

  time.sleep(0.5)

  task1 = greenlet(walk)

  task2 = greenlet(speak)

  def main():

  task1.switch()

  if __name__ == "__main__":

  main()

  上述代码的执行结果为:

  Steve Jobs was walking around the auditorium…

  Steve Jobs was introducing his new iPhone…

  Steve Jobs was walking around the auditorium…

  Steve Jobs was introducing his new iPhone…

  Steve Jobs was walking around the auditorium…

  Steve Jobs was introducing his new iPhone…

  Steve Jobs was walking around the auditorium…

  2.3 greenlet与线程、进程

  在任何时候都只有一个“greenlet”在执行;

  “greenlet”可以和Python中的线程结合使用,此时,一个线程中包含一个独立的主“greenlet”和数个子“greenlet”;

  程序执行时,不能在属于不同线程的“greenlet”之间“切换”;

  系统在切换“greenlet”时,其执行时序是根据程序代码显式确定的,即不同“greenlet”执行时序是程序确定的;

  Python中使用multiprocessing和threading模块创建的多个进程或线程,其调用时序都是由操作系统隐式确定的。

  3. 通过gevent实现多任务

  3.1 gevent简介

  gevent是Python中另一个可用来实现多任务的模块,gevent基于“greenlet”实现,这一点可从gevent模块源码窥见一斑:gevent模块中有一个名为Greenlet的类,该类继承了greenlet模块中的greenlet类(继承可以提高代码重用性,便于开发者在已有的基础上扩展新功能)。

  至于为什么Python中有了“greenlet”模块作为multiprocessing以及threading模块实现多任务的功能补充,原因在于:

  上述通过yield和greenlet虽然都实现了多任务,且通过这两种方式实现多任务所占用资源比使用进程和线程都要少得多,但问题在于:如果某一个任务执行时间过长(如:在某一个任务中有一个长时间的延时操作、阻塞操作),则另外一个任务将迟迟得不到切换执行而被阻塞,从而可能降低多任务的总体效率。而gevent实现多任务的一个最大特点是:使用gevent创建的多任务对象,在遇到任何延时类操作都会自动切换任务,类似于程序异步执行。

  3.2 同步和异步执行

  程序并发的核心在于,一个大的任务可以被分解为一系列小的子任务,这些小的子任务在时序上同时(simultaneously)执行或程序之间的调用是异步(asynchronously,异步是指一个程序调用另一个程序时,不等待被调用程序执行结束而立即就返回一个结果并继续执行)进行的,而不是一次执行一个子任务或程序之间的调用关系是同步的(同步是指一个程序调用另一个程序时,必须等待被调用程序执行结束返回一个结果,调用程序才可以继续向下执行)。

  关于同步和异步的概念,请见下方代码。

  在使用multiprocessing以及threading模块完成多任务时,多个任务一般为同时执行;

  在使用gevent完成多任务时,多个任务一般类似于异步执行,这在使用gevent的场景下被称为“上下文切换(context switch)”,切换的时机是显式但自动的(程序遇到延时阻塞类操作就会切换任务),即遇到延时、阻塞类操作,程序就会自动切换上下文。

  如下列程序所示,当程序遇到延时操作时(此处的延时、阻塞类操作都在gevent模块中),就会自动切换上下文:

  import gevent

  import random

  import time

  tasks_info_list = list()

  def task(task_id):

  """

  Some non-deterministic task

  """

  t_task_start = time.time()

  print("Task %d is started" % task_id)

  # sleep()为延时操作,使用gevent的多任务程序运行至此处会自动切换上下文

  gevent.sleep(random.randint(1, 10) * 0.1)

  print('Task %d done' % task_id)

  tasks_info_list.append("Time elapses %.3f for task%d" %

  (time.time() - t_task_start, task_id))

  def synchronous():

  for i in range(5):

  task(i)

  def asynchronous():

  # 列表推导式

  greenlets = [gevent.spawn(task, i) for i in range(5)]

  gevent.joinall(greenlets)

  def main():

  t_sync_start = time.time()

  print('Synchronous:')

  synchronous()郑州正规人流医院 http://www.zykdrl120.com

  print("%.3f second(s) elapse for all synchronous tasks..." % (time.time() - t_sync_start))

  for each in tasks_info_list:

  print(each)

  t_async_start = time.time()

  print('Asynchronous:')

  asynchronous()

  print("%.3f second elapses for all asynchronous tasks..." % (time.time() - t_async_start))

  # 仅打印异步执行任务时添加的任务信息

  for each in tasks_info_list[len(tasks_info_list) - 5: len(tasks_info_list)]:

  print(each)

  if __name__ == '__main__':

  main()

  上述代码的运行流程和结果如下所示:

  Synchronous:

  Task 0 is started

  Task 0 done

  Task 1 is started

  Task 1 done

  Task 2 is started

  Task 2 done

  Task 3 is started

  Task 3 done

  Task 4 is started

  Task 4 done

  2.705 second(s) elapse for all synchronous tasks…

  Time elapses 0.301 for task0

  Time elapses 0.101 for task1

  Time elapses 0.901 for task2

  Time elapses 0.501 for task3

  Time elapses 0.901 for task4

  Asynchronous:

  Task 0 is started

  Task 1 is started

  Task 2 is started

  Task 3 is started

  Task 4 is started

  Task 0 done

  Task 3 done

  Task 1 done

  Task 4 done

  Task 2 done

  0.702 second elapses for all asynchronous tasks…

  Time elapses 0.101 for task0

  Time elapses 0.301 for task3

  Time elapses 0.602 for task1

  Time elapses 0.602 for task4

  Time elapses 0.702 for task2

  分析上述代码及其运行情况可知:

  多个任务同步执行的效果是顺序进行的,且运行总时间为每个任务执行时间的总和;

  多个任务异步执行时,各个任务同样是按照程序指定的顺序启动,并尝试按照顺序执行一个任务后再执行下一个,但因为使用gevent的缘故,将导致某一个任务在遇到延时操作后切换到程序启动时指定的下一个任务,所以运行总时间为多个任务中执行时间最长的那个。

  需要说明的是,上述代码的第22行使用的spawn()方法相当于Greenlet类中的类方法spawn(),用来创建并启动一个greenlet对象。

  3.3 gevent实现多任务

  实际上,想使用gevent发挥其最大优势的方法,是将其用于网络以及和IO相关的多任务操作上,程序将会以一种时间效率最优的时序完成多个任务,而gevent屏蔽了所有的实现细节,以确保你的多个网络或IO相关任务可以在某一个任务遇到任何阻塞或延时类操作时,自动切换到别的任务中执行。

  如下面的示例代码中,select()函数用于以阻塞的方式轮询各种文件描述符,如果使用gevent,则某个任务在执行到select()函数时会直接自动切换到别的任务:

  import time

  import gevent

  from gevent import select

  start = time.time()

  tic = lambda: 'at %1.1f seconds' % (time.time() - start)

  def gr1():

  print('Started Polling in gr1: %s' % tic())

  select.select([], [], [], 3)

  print('Ended Polling in gr1: %s' % tic())

  def gr2():

  print('Started Polling in gr2: %s' % tic())

  select.select([], [], [], 2)

  print('Ended Polling in gr2: %s' % tic())

  def gr3():

  print("Hey lets do some stuff while the greenlets poll in gr3, %s" % tic())

  gevent.sleep(1)

  print("Inside gr3...")

  gevent.joinall([

  gevent.spawn(gr1),

  gevent.spawn(gr2),

  gevent.spawn(gr3),

  ])

  上述程序的运行结果为:

  Started Polling in gr1: at 0.0 seconds

  Started Polling in gr2: at 0.0 seconds

  Hey lets do some stuff while the greenlets poll in gr3, at 0.0 seconds

  Inside gr3…

  Ended Polling in gr2: at 2.0 seconds

  Ended Polling in gr1: at 3.0 seconds

  3.4 gevent中的Moneypatching

  不知你是否注意到了,上述的阻塞、延时类操作如sleep、select都是gevent模块中的,这是因为,标准模块中的阻塞、延时类操作不能自动切换任务,而gevent对这些标准模块中的操作进行了改造和封装,这就叫做Monkeypatching(狗皮膏药?)。

  事实上,Monkeypatching利用了Python语言的一大特点,即:Python允许绝大多数对象能在运行时被修改,包括模块、类甚至函数。通常,这对于开发者来说是灾难性的,因为程序出错时将非常难以排查。但是,在部分情形下,如果一个模块需要修改其基本特性,则可以利用这一语言特性实现,这就是Moneypatching的实现基础。

  gevent的Monkeypatching可以将大多数的阻塞延时功能进行修改,如:socket、ssl、threading、select,以达到多任务协作的目的。

  实际上,我们不需要在每次遇到延时阻塞类操作时,都显式的通过gevent.操作名来指定,而且在某一些情况下也不现实,因为已经有很多基于标准阻塞模块开发的第三方模块,我们无法将实现将所有模块的源码进行对应修改。

  针对上述问题,gevent提供一个一次性修改的简单语法,即:

  首先,从gevent模块中导入monkey模块;

  然后,调用monkey模块中的patch_all()函数;

  最后,在程序中所有的阻塞延时类操作就可以继续使用标准模块的操作。

  4. 参考资料

  greenlet: Lightweight concurrent programming

  gevent For the Working Python Developer

posted @ 2020-05-07 16:40  网管布吉岛  阅读(202)  评论(0编辑  收藏  举报