天天看點

Python協程:概念及其用法

Python協程:概念及其用法

真正有知識的人的成長過程,就像麥穗的成長過程:麥穗空的時候,麥子長得很快,麥穗驕傲地高高昂起,但是,麥穗成熟飽滿時,它們開始謙虛,垂下麥芒。

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

對于協程,我表示其效率确非多線程能比,但本人對此了解并不深入,是以最近幾日參考了一些資料,學習整理了一番,在此分享出來僅供大家參考,如有謬誤請指正,多謝。申明:本文介紹的協程是入門級别,大神請繞道而行,謹防入坑。

文章思路:本文将先介紹協程的概念,然後分别介紹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是第三方庫,通過greenlet實作協程,其基本思想:

當一個greenlet遇到io操作時,比如通路網絡,就自動切換到其他的greenlet,等到io操作完成,再在适當的時候切換回來繼續執行。由于io操作非常耗時,經常使程式處于等待狀态,有了gevent為我們自動切換協程,就保證總有greenlet在運作,而不是等待io。

install

pip install gevent 

最新版貌似支援windows了,之前測試好像windows上運作不了……

usage

首先來看一個簡單的爬蟲例子:

#! -*- coding:utf-8 -*- 

import gevent 

from gevent import monkey;monkey.patch_all() 

import urllib2 

def get_body(i): 

print "start",i 

urllib2.urlopen("http://cn.bing.com") 

print "end",i 

tasks=[gevent.spawn(get_body,i) for i in range(3)] 

gevent.joinall(tasks)  

運作結果:

start 0 

start 1 

start 2 

end 2 

end 0 

end 1  

說明:從結果上來看,執行get_body的順序應該先是輸出”start”,然後執行到urllib2時碰到io堵塞,則會自動切換運作下一個程式(繼續執行get_body輸出start),直到urllib2傳回結果,再執行end。也就是說,程式沒有等待urllib2請求網站傳回結果,而是直接先跳過了,等待執行完畢再回來擷取傳回值。值得一提的是,在此過程中,隻有一個線程在執行,是以這與多線程的概念是不一樣的。

換成多線程的代碼看看:

import threading 

for i in range(3): 

t=threading.thread(target=get_body,args=(i,)) 

t.start()  

end 1 

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)

python3.4以後引入了asyncio子產品,可以很好的支援協程。

asynico

asyncio是python 3.4版本引入的标準庫,直接内置了對異步io的支援。asyncio的異步操作,需要在coroutine中通過yield from完成。

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

import asyncio 

@asyncio.coroutine 

def test(i): 

print("test_1",i) 

r=yield from asyncio.sleep(1) 

print("test_2",i) 

loop=asyncio.get_event_loop() 

tasks=[test(i) for i in range(5)] 

loop.run_until_complete(asyncio.wait(tasks)) 

loop.close()  

test_1 3 

test_1 4 

test_1 0 

test_1 1 

test_1 2 

test_2 3 

test_2 0 

test_2 2 

test_2 4 

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。

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

async def test(i): 

await asyncio.sleep(1) 

運作結果與之前一緻。

說明:與前一節相比,這裡隻是把yield from換成了await,@asyncio.coroutine換成了async,其餘不變。

同python2.x用法一樣。

協程vs多線程

如果通過以上介紹,你已經明白多線程與協程的不同之處,那麼我想測試也就沒有必要了。因為當線程越來越多時,多線程主要的開銷花費線上程切換上,而協程是在一個線程内切換的,是以開銷小很多,這也許就是兩者性能的根本差異之處吧。(個人觀點)

異步爬蟲

也許關心協程的朋友,大部分是用其寫爬蟲(因為協程能很好的解決io阻塞問題),然而我發現常用的urllib、requests無法與asyncio結合使用,可能是因為爬蟲子產品本身是同步的(也可能是我沒找到用法)。那麼對于異步爬蟲的需求,又該怎麼使用協程呢?或者說怎麼編寫異步爬蟲?

給出幾個我所了解的方案:

grequests (requests子產品的異步化)

爬蟲子產品+gevent(比較推薦這個)

aiohttp (這個貌似資料不多,目前我也不太會用)

asyncio内置爬蟲功能 (這個也比較難用)

協程池

作用:控制協程數量

from bs4 import beautifulsoup 

import requests 

from gevent import monkey, pool 

monkey.patch_all() 

jobs = [] 

links = [] 

p = pool.pool(10) 

urls = [ 

    'http://www.google.com', 

    # ... another 100 urls 

def get_links(url): 

    r = requests.get(url) 

    if r.status_code == 200: 

        soup = beautifulsoup(r.text) 

        links + soup.find_all('a') 

for url in urls: 

    jobs.append(p.spawn(get_links, url)) 

gevent.joinall(jobs)  

本文都是一些自學時的筆記,分享給新手朋友,僅供參考

作者:佚名

來源:51cto