python 协程 及其与python多线程的区别和联系

协程(coroutine)又称微线程,纤程,是种用户级别的轻量级线程。 

    协程拥有自己的寄存器上下文和栈。协程调度切换时候,将寄存器上下文和栈保存到其他地方,等待切换回来的时候恢复,并从之前保存的寄存器上下文 和 栈继续工作。 

    并发编程中,协程与 线程类似,每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据及资源池。

    协程需要操作员单独写调度逻辑,对CPU来说,协程也就是单线程,因此CPU 不需要考虑怎么调度、切换上下文,省去了CPU开销,所以,协程又在一定程度上好于多线程。


python中实现协程:

    python使用yield 提供对协程的基本支持,但是,第三方的 gevent库能更好地提供该服务,gevent有比较完善的协程支持。

    geve 是基于协程的python网络函数库,使用greenlet 在libev事件循环顶部提供一个有高级别并发性的API。

    特点:

    (1)基于libev 的快速事件循环,Linux上的epoll机制。

    (2)基于greenlet的轻量级执行单元。

    (3)API 复用了python标准库的内容。

    (4)支持SSL的协作式sockets。

    (5)可以通过线程池或者c-ares 实现DNS查询。

    (6)通过 monkey patching功能使得第三方模块编程协作式。


    gevent支持协程,其实也可以说是greenlet实现的工作切换。

    greenlet工作流程如下:如果访问网路的I/O操作出现阻塞时,greenlet就显式切换到另外一个没有被阻塞的代码段执行,直到原先的阻塞状态消失后,再自动切换会原来的代码段继续处理。 可以说,greenlet是在更合理地安排串行工作方式。

    同时,由于IO 操作比较耗时,经常是程序处于等待状态,gevent自动切换协程后,能够保证总有greenlet在运行,而不需要等待IO完成,这是协程比一般多线程效率高的原因。

    IO操作是自动完成的,所以gevent 需要修改python的一些自带标准库,将一些常见的阻塞,如:socket、select等地方实现协程跳转,这一过程可以通过monkey patch完成。

如下代码可以显示 gevent的使用流程:(python版本: 3.6  操作系统环境: windows10)

from gevent import monkey
monkey.patch_all()
import gevent
import urllib.request

def run_task(url):
    print("Visiting %s " % url)
    try:
        response = urllib.request.urlopen(url)
        url_data = response.read()
        print("%d bytes received from %s " % (len(url_data), url))
    except Exception as e:
        print(e)

if __name__ == "__main__":
    urls = ["https://stackoverflow.com/", "http://www.cnblogs.com/", "http://github.com/"]
    greenlets = [gevent.spawn(run_task, url) for url in urls]
    gevent.joinall(greenlets)
Visiting https://stackoverflow.com/ 
Visiting http://www.cnblogs.com/ 
Visiting http://github.com/ 
46412 bytes received from http://www.cnblogs.com/ 
54540 bytes received from http://github.com/ 
251799 bytes received from https://stackoverflow.com/ 

    gevent的spawn方法可以看做是用来形成协程,joinall 方法相当于添加协程任务并启动运行。由结果可以看到,3个网络请求并发执行,而且结束顺序却不一致,但是却只有一个线程。

    gevent还提供池。如果拥有动态数量的 greenlet需要进行并发管理,可以使用池 来处理大量的网络请求 及 IO操作。

    如下是 gevent的pool对象,修改如上的多网络请求示例:

from gevent import monkey
monkey.patch_all()
from gevent.pool import Pool
import urllib.request

def run_task(url):
    print("Visiting %s " % url)
    try:
        response = urllib.request.urlopen(url)
        url_data = response.read()
        print("%d bytes reveived from %s " %(len(url_data), url))
    except Exception as e:
        print(e)
    
    return ("%s read finished.." % url)

if __name__ == "__main__":
    pool = Pool(2)
    urls = ["https://stackoverflow.com/", 
            "http://www.cnblogs.com/", 
            "http://github.com/"]
    results = pool.map(run_task, urls)
    print(results)

Visiting https://stackoverflow.com/ 
Visiting http://www.cnblogs.com/ 
46416 bytes reveived from http://www.cnblogs.com/ 
Visiting http://github.com/ 
253375 bytes reveived from https://stackoverflow.com/ 
54540 bytes reveived from http://github.com/ 
['https://stackoverflow.com/ read finished..', 'http://www.cnblogs.com/ read finished..', 'http://github.com/ read finished..']
    由结果来看,Pool 对象对协程的并发数量进行了管理,先访问前两个,当其中一个任务完成了,再继续执行第三个请求。



猜你喜欢

转载自blog.csdn.net/WJUNSING/article/details/80226707