Python并发任务之协程

多线程会消耗系统资源,多进程则有GIL限制和影响,Python中还有一种高效地执行并发任务的方式,那就是协程了。

协程也成为微线程,比线程更轻量级。协程通过在单线程内进行函数执行切换来进行并发。也就是说,协程是单线程执行,并且在线程内函数之间的执行也是可以切换的。

如果说多进程、多线程是抢占式的任务处理方式,那么协程就是协作式的任务处理方式。协程虽然是单线程,但是通过协作切换来充分利用CPU,所以也可以实现高并发的场景。多进程时程序启动多个和自己一样的子进程,每个进程都有自己的GIL,所以多进程在多核CPU上不会受GIL的影响。多线程时程序会通过进程内的主线程来启动子进程。因为在同一个进程内,所有的线程都共同一个GIL,所以多线程在多核CPU上变为串行执行。协程是通过分割主进程的计算方法来实现的,在单线程的情况下,协程始终都能获得GIL,所以不会受GIL的影响。如果希望协程利用多核CPU的计算能力,那么只能通过多进程和协程配合的模式。

协程利用Python的实现,使用关键字yield来保持函数现场的能力,并在下次调用时再恢复函数的现场。即给yield添加接收信息的能力,那么就可以实现协程了:

伪代码如下:

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return 
        print("Consumer %s ..." % n)
        r = '200 OK'

def produce(c):
    c.next()
    n = 0
    while n < 3:
        n += 1
        print("Producing %s..." % n)
        r = c.send(n)
        print("Consumer return: %s" % r)
    c.close()

if __name__ == "__main__":
    c = comsumer()
    produce(c)

上面伪代码consumer是协程子程序,可以通过send方法切换执行子程序,并且可以附带一个参数过去,但协程子程序执行结束时,会返回执行结果给调用函数。最后处理完所有任务之后通过close方法关闭协程子程序。

协程的执行其实就是单个线程不断地切换执行函数,与普通函数调用不同的是,协程不是依次调用的关系,而是互相切换的关系。相当于函数之间按照规律相互协作来完成一个任务,所以被称为协程。

那么协程是否比多线程模式效率更高?Cython解析器的多线程有GIL限制,而协程没有。多线程在上下文切换时需要消耗资源,而协程切换则不需要。通过与多进程的配合,协程也可以充分利用多核CPU。

真正要使用协程来实现阻塞切换则需要第三方支持协程模式,并且封装关于协程的一切处理。可以使用gevent,它可以方便地协助使用协程处理高并发:

import sys
from urllib.request import urlopen
import gevent
from gevent import monkey
monkey.patch_socket()

urls = [
    'http://www.baidu.com',
    'http://www.mi.com',
    'http://www.python.org'
]

def print_request(url):
    print('Starting %s' % url)
    data = urlopen(url).read()
    print('%s: %d bytes' % (url, len(data)))

if __name__ == "__main__":
    jobs = [gevent.spawn(print_request, url) for url in urls]
    gevent.wait(jobs)

执行结果如下:

Starting http://www.baidu.com
Starting http://www.mi.com
Starting http://www.python.org
http://www.baidu.com: 153287 bytes
http://www.mi.com: 330388 bytes
http://www.python.org: 48791 bytes
注:使用gevent时,需要先确保执行函数中的IO模块是非堵塞的,否则协程将不能并发执行。代码中的monkey.path_socket()方法就是用于动态地为Python的socket标准库打补丁。作用是让原本IO堵塞的socket模块变为非堵塞的形式。因为urllib是基于socket模块的,所以并发才会生效。

你可能感兴趣的:(Python)