多任务协程,用它一个 Python 控制胜局
2023-11-19 14:09:08
协程是一种特殊的执行方式,它允许在一个线程中交替执行多个任务,同时还允许任务之间相互协作。它最早起源于 20 世纪 60 年代,并从 20 世纪 90 年代开始流行。
协程的优点在于,它在并行的同时,还能保留并发性。这与多线程方法不同,多线程在 并行时,不会保留并发性,因此会出现多个线程同时读写同一个变量,这种竞争会产生死锁(deadlock)。
协程与并行框架
协程(coroutine) 和并行框架(Parallelism framework) 属于同一类,但实际上协程与并行框架有一个本质区别:
执行方式不同:并行框架中,多个任务是并发执行,然后按照某一特定的策略将任务合并到一起;协程在执行时则把任务进行切换,使其交替执行。
在绝大多数的场景,特别是当任务之间频繁互相协作时,协程要比多线程效率高。
协程的缺点则在于,它本质上只能在单核上执行任务,因此处理密集型任务时不如并行框架高效。
协程在 Python 的应用
在 Python 中,可以使用 gevent 和 greenlet 实现协程。
gevent 更通用,适用性更广, 它可以同时在 CPython 和 Jython 上使用,使得程序能在虚拟机和解释器上都能执行,它模拟类似协程的环境。
greenlet 是 Jython 上的原生协程实现,它将协程控制在 Java 虚拟机内部,实现效率比 gevent 要高。此外,greenlet 是 green 这个完整框架的一部分,green 也使用了 gevent,但它提供了更高层次的抽象,使其更易于理解和使用。
gevent 可以让脚本作者无需多任务处理和同步等工作即可轻松完成并发程序,从而让编码任务简单化并减少程序出问题的风险。
使用 gevent 需要安装模块 gevent,它是 gevent 框架的核心。gevent 的安装分两种,从源码安装和从包安装。
gevent 支持 select 和 select.epoll 系统调用来处理输入、 输出事件,因此适用于网络操作。而 greenlet 支持 eventlet 事件处理库,还支持 Java 和 Java 虚拟机,使用 Java 上的 native sockets 能达到很好的速度。
现在,我们可以说,在 Python 中 gevent 和 greenlet 是 gevent 更好的选择,在 Java中 greenlet 是更好的选择。
Python 协程示例
并行处理是软件应用非常常见的任务,Python 的多任务处理主要有两种方式,在基于 CPython 的解释器时可以选择 gevent,基于 Jython 时可选 greenlet 。
gevent 提供了与 Python 2.6 版本非常类似的 API,即 gevent 自己维护一组 event loop,然后使用 gevent.spawn() 和 gevent.join() 。因为 gevent.join() 是一个阻塞调用,我们可以使用 yield 来实现一个协程。
greenlet 提供了基于 yield 的 API 接口,这也是 greenlet 最大的优势。 greenlet 利用 yield,我们可以实现非常简单的交替执行、串行执行、并行执行。
用 yield from 实现协程比较简单,看起来也很优雅,适用于需要在多任务之间频繁切换的程序中。
一个更普遍的使用 yield 的范例是实现迭代并过滤一个列表。在一个列表中,元素彼此独立,因此可以并行处理。
我们可以用 yield from 实现一个简单的客户端,并用 gevent 模块并行执行多个 HTTP 请求。同时,还可以选择 greenlet 来实现并行客户端,而 greenlet 比 gevent 更易理解和使用。此外,还可以选择 green 来实现并行客户端。
选择 gevent 的一大优点在于,它将协程控制在用户态,它不会让程序死锁,没有死锁就意味着不必担心死锁的出现。另一个优点是,使用 gevent 能够实现跨平台、跨语言,用 gevent 模块实现的程序能够在 CPython 和 Jython 上执行。
我们使用 Python 的 gevent 模块,可以比直接使用网络 I/O 操作更易于理解和实现。
以下是基于 gevent 的并行处理的两则完整的示例。
gevent 提供了非常实惠的 HTTP 客户端,我们使用 gevent 来实现并行客户端。 这个示例是基于 gevent 模块实现的,它将使用它内置的 HTTP 客户端向预先设置的一组远程 Web 应用程序发送请求,随后这些远程 Web 应用程序将对这些请求做相应的处理。最后,gevent 会按照响应的顺序汇总并显示这些远程应用程序返回来的结果。
gevent_client.py 代码的整体框架十分清晰,就是初始化任务、请求任务、执行任务、收尾任务. gevent 模块为我们实现了并行的框架,将任务交给 gevent 来执行,然后针对这组任务进行合并,这样 gevent 可以动态调整和合并任务。
gevent 提供了非常实惠的 HTTP 客户端,我们使用 gevent 来实现并行客户端。这个示例是基于 gevent 模块实现的,它将使用它内置的 HTTP 客户端向预先设置的一组远程 Web 应用程序发送请求,随后这些远程 Web 应用程序将对这些请求做相应的处理。最后,gevent 会按照响应的顺序汇总并显示这些远程应用程序返回来的结果。
greenlet 提供了基于 yield 的 API 接口,这是 greenlet 最大的优势。 greenlet 利用 yield,我们可以实现非常简单的交替执行、串行执行、并行执行。
我们使用 Python 的 greenlet 模块,可以比直接使用网络 I/O 操作更易于理解和实现。
用 gevent 模块实现并行客户端的示例更具代表性, greenlet 的安装更简易,直接用 pip 命令安装便可。 greenlet 模块非常易于理解和使用,greenlet_client.py 实现的并行客户端,与 gevent_client.py 代码一样,也是基于并行框架 gevent 实现的。
gevent_server.py 代码模拟了远程 Web 应用程序,它使用 gevent 实现了 HTTP 服务端, gevent_server.py 代码很容易实现,我们只需要用 greenlet 模块启动一个网络服务端并设置一个 HTTP 处理函数来相应 HTTP 请求,这个处理函数在 HTTP 请求的时候会返回响应,greenlet 模块会处理响应返回给 gevent_client.py。
greenlet 模块需要额外的 greenlet 包,对 greenlet 包进行安装才可以使用。greenlet_server.py 代码和 greenlet_client.py 代码一样,它使用 greenlet 实现并行处理。
最后,使用 gevent 模块我们很易构建并行客户端,它允许你能够很容易、较少代码编写并行的 Web 客户端。使用 greenlet 模块也很易实现,它提供了更高级的 API 接口。
并行的框架提供并行的处理,任务在并行时,task.spawn() 任务执行后,task.join() 的结果就是一个 return 的值。任务间的切换在任何时间点,可能产生所有中间结果到 task.join() 结果的多个状态的组合。
协程提供并行的处理,切换一个任务时,没有执行一个 yield 的结果,需要继续执行 task.send(value) 到下一次的时候 task.send(value) 即可,当继续执行 yield task.join() 的结果时,就是上一个执行task.send(value) 任务的 return 的值。
使用并行框架,task.join() 只是一个简单的任务,用来实现并行的处理。 task.spawn() 中的任务是一定顺序的。使用 gevent 和 greenlet,通过 yield 实现交替执行。