Python协程:概念及其用法

真正有知识的人的成长过程,就像麦穗的成长过程:麦穗空的时候,麦子长得很快,麦穗骄傲地高高昂起,但是,麦穗成熟饱满时,它们开始谦虚,垂下麦芒。

——蒙田《蒙田随笔全集》

上篇《Python 多线程鸡年不鸡肋》论述了关于python多线程是否是鸡肋的问题,得到了一些网友的认可,当然也有一些不同意见,表示协程比多线程不知强多少,在协程面前多线程算是鸡肋。好吧,对此我也表示赞同,然而上篇我论述的观点不在于多线程与协程的比较,而是在于IO密集型程序中,多线程尚有用武之地。

对于协程,我表示其效率确非多线程能比,但本人对此了解并不深入,因此最近几日参考了一些资料,学习整理了一番,在此分享出来仅供大家参考,如有谬误请指正,多谢。申明:本文介绍的协程是入门级别,大神请绕道而行,谨防入坑。

文章思路:本文将先介绍协程的概念,然后分别介绍Python2.x与3.x下协程的用法,最终将协程与多线程做比较并介绍异步爬虫模块。

协程

概念

协程,又称微线程,纤程,英文名Coroutine。协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行。

优势

  • 执行效率极高,因为子程序切换(函数)不是线程切换,由程序自身控制,没有切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显。
  • 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁,因此执行效率高很多。

说明:协程可以处理IO密集型程序的效率问题,但是处理CPU密集型不是它的长处,如要充分发挥CPU利用率可以结合多进程+协程。

以上只是协程的一些概念,可能听起来比较抽象,那么我结合代码讲一讲吧。这里主要介绍协程在Python的应用,Python2对协程的支持比较有限,生成器的yield实现了一部分但不完全,gevent模块倒是有比较好的实现;Python3.4以后引入了asyncio模块,可以很好的使用协程。

Python2.x协程

python2.x协程应用:

  • yield
  • gevent

python2.x中支持协程的模块不多,gevent算是比较常用的,这里就简单介绍一下gevent的用法。

Gevent

gevent是第三方库,通过greenlet实现协程,其基本思想:

当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。

Install


  1. pip install gevent 

最新版貌似支持windows了,之前测试好像windows上运行不了……

Usage

首先来看一个简单的爬虫例子:


  1. #! -*- coding:utf-8 -*- 
  2.  
  3. import gevent 
  4.  
  5. from gevent import monkey;monkey.patch_all() 
  6.  
  7. import urllib2 
  8.  
  9. def get_body(i): 
  10.  
  11. print "start",i 
  12.  
  13. urllib2.urlopen("http://cn.bing.com") 
  14.  
  15. print "end",i 
  16.  
  17. tasks=[gevent.spawn(get_body,i) for i in range(3)] 
  18.  
  19. gevent.joinall(tasks)  

运行结果:


  1. start 0 
  2.  
  3. start 1 
  4.  
  5. start 2 
  6.  
  7. end 2 
  8.  
  9. end 0 
  10.  
  11. end 1  

说明:从结果上来看,执行get_body的顺序应该先是输出”start”,然后执行到urllib2时碰到IO堵塞,则会自动切换运行下一个程序(继续执行get_body输出start),直到urllib2返回结果,再执行end。也就是说,程序没有等待urllib2请求网站返回结果,而是直接先跳过了,等待执行完毕再回来获取返回值。值得一提的是,在此过程中,只有一个线程在执行,因此这与多线程的概念是不一样的。

换成多线程的代码看看:


  1. import threading 
  2.  
  3. import urllib2 
  4.  
  5. def get_body(i): 
  6.  
  7. print "start",i 
  8.  
  9. urllib2.urlopen("http://cn.bing.com") 
  10.  
  11. print "end",i 
  12.  
  13. for i in range(3): 
  14.  
  15. t=threading.Thread(target=get_body,args=(i,)) 
  16.  
  17. t.start()  

运行结果:


  1. start 0 
  2.  
  3. start 1 
  4.  
  5. start 2 
  6.  
  7. end 1 
  8.  
  9. end 2 
  10.  
  11. end 0  

说明:从结果来看,多线程与协程的效果一样,都是达到了IO阻塞时切换的功能。不同的是,多线程切换的是线程(线程间切换),协程切换的是上下文(可以理解为执行的函数)。而切换线程的开销明显是要大于切换上下文的开销,因此当线程越多,协程的效率就越比多线程的高。(猜想多进程的切换开销应该是最大的)

Gevent使用说明

  • monkey可以使一些阻塞的模块变得不阻塞,机制:遇到IO操作则自动切换,手动切换可以用gevent.sleep(0)(将爬虫代码换成这个,效果一样可以达到切换上下文)
  • gevent.spawn 启动协程,参数为函数名称,参数名称
  • gevent.joinall 停止协程

Python3.x协程

为了测试Python3.x下的协程应用,我在virtualenv下安装了python3.6的环境。

python3.x协程应用:

  • asynico + yield from(python3.4)
  • asynico + await(python3.5)
  • gevent

Python3.4以后引入了asyncio模块,可以很好的支持协程。

asynico

asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。asyncio的异步操作,需要在coroutine中通过yield from完成。

Usage

例子:(需在python3.4以后版本使用)


  1. import asyncio 
  2.  
  3. @asyncio.coroutine 
  4.  
  5. def test(i): 
  6.  
  7. print("test_1",i) 
  8.  
  9. r=yield from asyncio.sleep(1) 
  10.  
  11. print("test_2",i) 
  12.  
  13. loop=asyncio.get_event_loop() 
  14.  
  15. tasks=[test(i) for i in range(5)] 
  16.  
  17. loop.run_until_complete(asyncio.wait(tasks)) 
  18.  
  19. loop.close()  

运行结果:


  1. test_1 3 
  2.  
  3. test_1 4 
  4.  
  5. test_1 0 
  6.  
  7. test_1 1 
  8.  
  9. test_1 2 
  10.  
  11. test_2 3 
  12.  
  13. test_2 0 
  14.  
  15. test_2 2 
  16.  
  17. test_2 4 
  18.  
  19. test_2 1  

说明:从运行结果可以看到,跟gevent达到的效果一样,也是在遇到IO操作时进行切换(所以先输出test_1,等test_1输出完再输出test_2)。但此处我有一点不明,test_1的输出为什么不是按照顺序执行的呢?可以对比gevent的输出结果(希望大神能解答一下)。

asyncio说明

@asyncio.coroutine把一个generator标记为coroutine类型,然后,我们就把这个coroutine扔到EventLoop中执行。

test()会首先打印出test_1,然后,yield
from语法可以让我们方便地调用另一个generator。由于asyncio.sleep()也是一个coroutine,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield
from拿到返回值(此处是None),然后接着执行下一行语句。

把asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的coroutine了,因此可以实现并发执行。

asynico/await

为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。

请注意,async和await是针对coroutine的新语法,要使用新的语法,只需要做两步简单的替换:

把@asyncio.coroutine替换为async;

把yield from替换为await。

Usage

例子(python3.5以后版本使用):


  1. import asyncio 
  2.  
  3. async def test(i): 
  4.  
  5. print("test_1",i) 
  6.  
  7. await asyncio.sleep(1) 
  8.  
  9. print("test_2",i) 
  10.  
  11. loop=asyncio.get_event_loop() 
  12.  
  13. tasks=[test(i) for i in range(5)] 
  14.  
  15. loop.run_until_complete(asyncio.wait(tasks)) 
  16.  
  17. loop.close()  

运行结果与之前一致。

说明:与前一节相比,这里只是把yield from换成了await,@asyncio.coroutine换成了async,其余不变。

gevent

同python2.x用法一样。

协程VS多线程

如果通过以上介绍,你已经明白多线程与协程的不同之处,那么我想测试也就没有必要了。因为当线程越来越多时,多线程主要的开销花费在线程切换上,而协程是在一个线程内切换的,因此开销小很多,这也许就是两者性能的根本差异之处吧。(个人观点)

异步爬虫

也许关心协程的朋友,大部分是用其写爬虫(因为协程能很好的解决IO阻塞问题),然而我发现常用的urllib、requests无法与asyncio结合使用,可能是因为爬虫模块本身是同步的(也可能是我没找到用法)。那么对于异步爬虫的需求,又该怎么使用协程呢?或者说怎么编写异步爬虫?

给出几个我所了解的方案:

  • grequests (requests模块的异步化)
  • 爬虫模块+gevent(比较推荐这个)
  • aiohttp (这个貌似资料不多,目前我也不太会用)
  • asyncio内置爬虫功能 (这个也比较难用)

协程池

作用:控制协程数量


  1. from bs4 import BeautifulSoup 
  2.  
  3. import requests 
  4.  
  5. import gevent 
  6.  
  7. from gevent import monkey, pool 
  8.  
  9. monkey.patch_all() 
  10.  
  11. jobs = [] 
  12.  
  13. links = [] 
  14.  
  15. p = pool.Pool(10) 
  16.  
  17. urls = [ 
  18.  
  19.     'http://www.google.com', 
  20.  
  21.     # ... another 100 urls 
  22.  
  23.  
  24. def get_links(url): 
  25.  
  26.     r = requests.get(url) 
  27.  
  28.     if r.status_code == 200: 
  29.  
  30.         soup = BeautifulSoup(r.text) 
  31.  
  32.         links + soup.find_all('a') 
  33.  
  34. for url in urls: 
  35.  
  36.     jobs.append(p.spawn(get_links, url)) 
  37.  
  38. gevent.joinall(jobs)  

本文都是一些自学时的笔记,分享给新手朋友,仅供参考

作者:佚名

来源:51CTO

时间: 2024-09-20 14:32:58

Python协程:概念及其用法的相关文章

python协程用法实例分析

  本文实例讲述了python协程用法.分享给大家供大家参考.具体如下: 把函数编写为一个任务,从而能处理发送给他的一系列输入,这种函数称为协程 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def print_matchs(matchtext): print "looking for",matchtext while True: line = (yield) #用 yield语句并以表达式(yield)的形式创建协程 if matchtext in

谈谈Python协程技术的演进

一.引言 1. 存储器山 存储器山是 Randal Bryant 在<深入理解计算机系统>一书中提出的概念. 基于成本.效率的考量,计算机存储器被设计成多级金字塔结构,塔顶是速度最快.成本最高的 CPU 内部的寄存器(一般几 KB)与高速缓存,塔底是成本最低.速度最慢的广域网云存储(如百度云免费 2T ) 存储器山的指导意义在于揭示了良好设计程序的必要条件是需要有优秀的局部性: 时间局部性:相同时间内,访问同一地址次数越多,则时间局部性表现越佳; 空间局部性:下一次访问的存储器地址与上一次的访

Python协程:从yield/send到async/await

这个文章理好了脉落. http://python.jobbole.com/86069/ 我练 习了一番,感受好了很多... Python由于众所周知的GIL的原因,导致其线程无法发挥多核的并行计算能力(当然,后来有了multiprocessing,可以实现多进程并行),显得比较鸡肋.既然在GIL之下,同一时刻只能有一个线程在运行,那么对于CPU密集的程序来说,线程之间的切换开销就成了拖累,而以I/O为瓶颈的程序正是协程所擅长的: 多任务并发(非并行),每个任务在合适的时候挂起(发起I/O)和恢复

由浅入深讲解Python yield的概念和用法

您可能听说过,带有 yield 的函数在 Python 中被称之为 generator(生成器),何谓 generator ? 我们先抛开 generator,以一个常见的编程题目来展示 yield 的概念. 如何生成斐波那契數列 斐波那契(Fibonacci)數列是一个非常简单的递归数列,除第一个和第二个数外,任意一个数都可由前两个数相加得到.用计算机程序输出斐波那契數列的前 N 个数是一个非常简单的问题,许多初学者都可以轻易写出如下函数: 清单 1. 简单输出斐波那契數列前 N 个数 def

理解Python的协程机制-Yield

根据PEP-0342 Coroutines via Enhanced Generators,原来仅仅用于生成器的yield关键字被扩展,成为Python协程实现的一部分.而之所以使用协程,主要是出于性能的考虑:一个活跃的Python线程大约占据8MB内存,而一个活跃线程只使用1KB不到内存.对于IO密集型的应用,显然轻量化的协程更适用. 概述 原来,yield是一个statement,即和return一样的语句,但是在PEP-0342后,yield statement被改造为了yield exp

一个使用 asyncio 协程的网络爬虫(二)

协程 还记得我们对你许下的承诺么?我们可以写出这样的异步代码,它既有回调方式的高效,也有多线程代码的简洁.这个结合是同过一种称为协程coroutine的模式来实现的.使用 Python3.4 标准库 asyncio 和一个叫"aiohttp"的包,在协程中获取一个网页是非常直接的( @asyncio.coroutine 修饰符并非魔法.事实上,如果它修饰的是一个生成器函数,并且没有设置 PYTHONASYNCIODEBUG 环境变量的话,这个修饰符基本上没啥用.它只是为了框架的其它部分

Python3.5 协程原理

本文讲的是Python3.5 协程原理, 作为 Python 核心开发者之一,让我很想了解这门语言是如何运作的.我发现总有一些阴暗的角落我对其中错综复杂的细节不是很清楚,但是为了能够有助于 Python 的一些问题和其整体设计,我觉得我应该试着去理解 Python 的核心语法和内部运作机制. 但是直到最近我才理解 Python 3.5 中 async/await 的原理.我知道 Python 3.3 中的 yield from 和 Python 3.4 中的asyncio 组合得来这一新语法.但

PHP实现协程

在服务器编程当中,为了实现异步,经常性的需要回调函数,例如以下这段代码 function send($value) { $data = process($value); onReceive($data); } function onReceive($recv_value) { var_dump($recv_value); } function process($value) { return $value+1; } $send_value = 1; send($send_value); 实现的东

python线程、进程和协程详解_python

引言 解释器环境:python3.5.1 我们都知道python网络编程的两大必学模块socket和socketserver,其中的socketserver是一个支持IO多路复用和多线程.多进程的模块.一般我们在socketserver服务端代码中都会写这么一句: server = socketserver.ThreadingTCPServer(settings.IP_PORT, MyServer) ThreadingTCPServer这个类是一个支持多线程和TCP协议的socketserver