摘要:所以與多線程相比,線程的數(shù)量越多,協(xié)程性能的優(yōu)勢(shì)越明顯。值得一提的是,在此過程中,只有一個(gè)線程在執(zhí)行,因此這與多線程的概念是不一樣的。
真正有知識(shí)的人的成長過程,就像麥穗的成長過程:麥穗空的時(shí)候,麥子長得很快,麥穗驕傲地高高昂起,但是,麥穗成熟飽滿時(shí),它們開始謙虛,垂下麥芒。
——蒙田《蒙田隨筆全集》
上篇論述了關(guān)于python多線程是否是雞肋的問題,得到了一些網(wǎng)友的認(rèn)可,當(dāng)然也有一些不同意見,表示協(xié)程比多線程不知強(qiáng)多少,在協(xié)程面前多線程算是雞肋。好吧,對(duì)此我也表示贊同,然而上篇我論述的觀點(diǎn)不在于多線程與協(xié)程的比較,而是在于IO密集型程序中,多線程尚有用武之地。
對(duì)于協(xié)程,我表示其效率確非多線程能比,但本人對(duì)此了解并不深入,因此最近幾日參考了一些資料,學(xué)習(xí)整理了一番,在此分享出來僅供大家參考,如有謬誤請(qǐng)指正,多謝。
申明:本文介紹的協(xié)程是入門級(jí)別,大神請(qǐng)繞道而行,謹(jǐn)防入坑。
文章思路:本文將先介紹協(xié)程的概念,然后分別介紹Python2.x與3.x下協(xié)程的用法,最終將協(xié)程與多線程做比較并介紹異步爬蟲模塊。
[](https://thief.one/2017/02/20/... "協(xié)程")協(xié)程 概念協(xié)程,又稱微線程,纖程,英文名Coroutine。協(xié)程的作用,是在執(zhí)行函數(shù)A時(shí),可以隨時(shí)中斷,去執(zhí)行函數(shù)B,然后中斷繼續(xù)執(zhí)行函數(shù)A(可以自由切換)。但這一過程并不是函數(shù)調(diào)用(沒有調(diào)用語句),這一整個(gè)過程看似像多線程,然而協(xié)程只有一個(gè)線程執(zhí)行。
[](https://thief.one/2017/02/20/... "優(yōu)勢(shì)")優(yōu)勢(shì)執(zhí)行效率極高,因?yàn)樽映绦蚯袚Q(函數(shù))不是線程切換,由程序自身控制,沒有切換線程的開銷。所以與多線程相比,線程的數(shù)量越多,協(xié)程性能的優(yōu)勢(shì)越明顯。
不需要多線程的鎖機(jī)制,因?yàn)橹挥幸粋€(gè)線程,也不存在同時(shí)寫變量沖突,在控制共享資源時(shí)也不需要加鎖,因此執(zhí)行效率高很多。
說明:協(xié)程可以處理IO密集型程序的效率問題,但是處理CPU密集型不是它的長處,如要充分發(fā)揮CPU利用率可以結(jié)合多進(jìn)程+協(xié)程。
以上只是協(xié)程的一些概念,可能聽起來比較抽象,那么我結(jié)合代碼講一講吧。這里主要介紹協(xié)程在Python的應(yīng)用,Python2對(duì)協(xié)程的支持比較有限,生成器的yield實(shí)現(xiàn)了一部分但不完全,gevent模塊倒是有比較好的實(shí)現(xiàn);Python3.4以后引入了asyncio模塊,可以很好的使用協(xié)程。
Python2.x協(xié)程python2.x協(xié)程應(yīng)用:
yield
gevent
python2.x中支持協(xié)程的模塊不多,gevent算是比較常用的,這里就簡(jiǎn)單介紹一下gevent的用法。
[](https://thief.one/2017/02/20/... "Gevent")Gevent gevent是第三方庫,通過greenlet實(shí)現(xiàn)協(xié)程,其基本思想:
當(dāng)一個(gè)greenlet遇到IO操作時(shí),比如訪問網(wǎng)絡(luò),就自動(dòng)切換到其他的greenlet,等到IO操作完成,再在適當(dāng)?shù)臅r(shí)候切換回來繼續(xù)執(zhí)行。由于IO操作非常耗時(shí),經(jīng)常使程序處于等待狀態(tài),有了gevent為我們自動(dòng)切換協(xié)程,就保證總有g(shù)reenlet在運(yùn)行,而不是等待IO。
pip install gevent
最新版貌似支持windows了,之前測(cè)試好像windows上運(yùn)行不了……
首先來看一個(gè)簡(jiǎn)單的爬蟲例子:
#! -*- 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)
運(yùn)行結(jié)果:
start 0 start 1 start 2 end 2 end 0 end 1
說明:從結(jié)果上來看,執(zhí)行g(shù)et_body的順序應(yīng)該先是輸出”start”,然后執(zhí)行到urllib2時(shí)碰到IO堵塞,則會(huì)自動(dòng)切換運(yùn)行下一個(gè)程序(繼續(xù)執(zhí)行g(shù)et_body輸出start),直到urllib2返回結(jié)果,再執(zhí)行end。也就是說,程序沒有等待urllib2請(qǐng)求網(wǎng)站返回結(jié)果,而是直接先跳過了,等待執(zhí)行完畢再回來獲取返回值。值得一提的是,在此過程中,只有一個(gè)線程在執(zhí)行,因此這與多線程的概念是不一樣的。
換成多線程的代碼看看:
import threading import urllib2 def get_body(i): print "start",i urllib2.urlopen("http://cn.bing.com") print "end",i for i in range(3): t=threading.Thread(target=get_body,args=(i,)) t.start()
運(yùn)行結(jié)果:
start 0 start 1 start 2 end 1 end 2 end 0
說明:從結(jié)果來看,多線程與協(xié)程的效果一樣,都是達(dá)到了IO阻塞時(shí)切換的功能。不同的是,多線程切換的是線程(線程間切換),協(xié)程切換的是上下文(可以理解為執(zhí)行的函數(shù))。而切換線程的開銷明顯是要大于切換上下文的開銷,因此當(dāng)線程越多,協(xié)程的效率就越比多線程的高。(猜想多進(jìn)程的切換開銷應(yīng)該是最大的)
monkey可以使一些阻塞的模塊變得不阻塞,機(jī)制:遇到IO操作則自動(dòng)切換,手動(dòng)切換可以用gevent.sleep(0)(將爬蟲代碼換成這個(gè),效果一樣可以達(dá)到切換上下文)
gevent.spawn 啟動(dòng)協(xié)程,參數(shù)為函數(shù)名稱,參數(shù)名稱
gevent.joinall 停止協(xié)程
[](https://thief.one/2017/02/20/... "Python3.x協(xié)程")Python3.x協(xié)程python3.5協(xié)程使用可以移步:Python3.5協(xié)程學(xué)習(xí)研究
為了測(cè)試Python3.x下的協(xié)程應(yīng)用,我在virtualenv下安裝了python3.6的環(huán)境。
python3.x協(xié)程應(yīng)用:
asynico + yield from(python3.4)
asynico + await(python3.5)
gevent
Python3.4以后引入了asyncio模塊,可以很好的支持協(xié)程。
[](https://thief.one/2017/02/20/... "asynico")asynicoasyncio是Python 3.4版本引入的標(biāo)準(zhǔn)庫,直接內(nèi)置了對(duì)異步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()
運(yùn)行結(jié)果:
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
說明:從運(yùn)行結(jié)果可以看到,跟gevent達(dá)到的效果一樣,也是在遇到IO操作時(shí)進(jìn)行切換(所以先輸出test_1,等test_1輸出完再輸出test_2)。但此處我有一點(diǎn)不明,test_1的輸出為什么不是按照順序執(zhí)行的呢?可以對(duì)比gevent的輸出結(jié)果(希望大神能解答一下)。
@asyncio.coroutine把一個(gè)generator標(biāo)記為coroutine類型,然后,我們就把這個(gè)coroutine扔到EventLoop中執(zhí)行。
test()會(huì)首先打印出test_1,然后,yield from語法可以讓我們方便地調(diào)用另一個(gè)generator。由于asyncio.sleep()也是一個(gè)coroutine,所以線程不會(huì)等待asyncio.sleep(),而是直接中斷并執(zhí)行下一個(gè)消息循環(huán)。當(dāng)asyncio.sleep()返回時(shí),線程就可以從yield from拿到返回值(此處是None),然后接著執(zhí)行下一行語句。
把a(bǔ)syncio.sleep(1)看成是一個(gè)耗時(shí)1秒的IO操作,在此期間,主線程并未等待,而是去執(zhí)行EventLoop中其他可以執(zhí)行的coroutine了,因此可以實(shí)現(xiàn)并發(fā)執(zhí)行。
為了簡(jiǎn)化并更好地標(biāo)識(shí)異步IO,從Python 3.5開始引入了新的語法async和await,可以讓coroutine的代碼更簡(jiǎn)潔易讀。
請(qǐng)注意,async和await是針對(duì)coroutine的新語法,要使用新的語法,只需要做兩步簡(jiǎn)單的替換:
把@asyncio.coroutine替換為async;
把yield from替換為await。
例子(python3.5以后版本使用):
import asyncio async def test(i): print("test_1",i) await 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()
運(yùn)行結(jié)果與之前一致。
說明:與前一節(jié)相比,這里只是把yield from換成了await,@asyncio.coroutine換成了async,其余不變。
同python2.x用法一樣。
[](https://thief.one/2017/02/20/... "協(xié)程VS多線程")協(xié)程VS多線程如果通過以上介紹,你已經(jīng)明白多線程與協(xié)程的不同之處,那么我想測(cè)試也就沒有必要了。因?yàn)楫?dāng)線程越來越多時(shí),多線程主要的開銷花費(fèi)在線程切換上,而協(xié)程是在一個(gè)線程內(nèi)切換的,因此開銷小很多,這也許就是兩者性能的根本差異之處吧。(個(gè)人觀點(diǎn))
[](https://thief.one/2017/02/20/... "異步爬蟲")異步爬蟲 也許關(guān)心協(xié)程的朋友,大部分是用其寫爬蟲(因?yàn)閰f(xié)程能很好的解決IO阻塞問題),然而我發(fā)現(xiàn)常用的urllib、requests無法與asyncio結(jié)合使用,可能是因?yàn)榕老x模塊本身是同步的(也可能是我沒找到用法)。那么對(duì)于異步爬蟲的需求,又該怎么使用協(xié)程呢?或者說怎么編寫異步爬蟲?
給出幾個(gè)我所了解的方案:
grequests (requests模塊的異步化)
爬蟲模塊+gevent(比較推薦這個(gè))
aiohttp (這個(gè)貌似資料不多,目前我也不太會(huì)用)
asyncio內(nèi)置爬蟲功能 (這個(gè)也比較難用)
[](https://thief.one/2017/02/20/... "協(xié)程池")協(xié)程池作用:控制協(xié)程數(shù)量
from bs4 import BeautifulSoup import requests import gevent 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)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/41907.html
摘要:如果在學(xué)習(xí)過程中有什么問題可以關(guān)注我公眾號(hào)琉憶編程庫給我留言。有興趣可以深入學(xué)習(xí)最后附上學(xué)習(xí)的知識(shí)結(jié)構(gòu)圖譜,可以按著下面的這個(gè)知識(shí)結(jié)構(gòu)圖進(jìn)行學(xué)習(xí)不一定完整,更多資料,面試題,都可以關(guān)注公眾號(hào)琉憶編程庫獲取。 你好,是我——琉憶。PHP程序員面試系列圖書作者。 作為一名PHP開發(fā)者過來人,也是經(jīng)歷了菜鳥到老手的過程,在此給那些想學(xué)PHP的同學(xué)指條路,即使你是轉(zhuǎn)行學(xué)PHP一樣可以學(xué)會(huì)PHP...
摘要:我是布小禪,一枚自學(xué)萌新,跟著我每天進(jìn)步一點(diǎn)點(diǎn)吧說了這么多暫時(shí)也就夠了,那么就告辭吧 文章目錄 ?? 前言 ??? 作者簡(jiǎn)介 ??文件操作?1??、open函數(shù)...
摘要:我們也提供了一種類似機(jī)器學(xué)習(xí)的模塊,幫助用戶挑選最適合自己的方法。從編程和機(jī)器學(xué)習(xí),到歷史,藝術(shù)和科學(xué),我們獲取知識(shí)的方式已經(jīng)發(fā)生改變。本譯文所涉法律后果均由本人承擔(dān)。本人同意簡(jiǎn)書平臺(tái)在接獲有關(guān)著作權(quán)人的通知后,刪除文章。 showImg(https://segmentfault.com/img/bV1qPe?w=700&h=382); 簡(jiǎn)評(píng):這個(gè)網(wǎng)站叫 Learn Anything,...
摘要:我就下決心想學(xué)前端了。接下來開始學(xué)習(xí)前端高級(jí)課程,老師幽默風(fēng)趣而又輕松的課堂教課,使我發(fā)現(xiàn)原來學(xué)習(xí)前端并不是一件很難的事情。先做個(gè)自我介紹,我13年考上一所很爛專科民辦的學(xué)校,學(xué)的是生物專業(yè),具體的學(xué)校名稱我就不說出來獻(xiàn)丑了。13年我就輟學(xué)了,我在那樣的學(xué)校,一年學(xué)費(fèi)要1萬多,但是根本沒有人學(xué)習(xí),我實(shí)在看不到希望,我就退學(xué)了。退學(xué)后我也迷茫,大專都沒有畢業(yè),我真的不知道我能干什么,我在糾結(jié)著...
閱讀 2155·2021-11-12 10:36
閱讀 2147·2021-09-03 10:41
閱讀 2761·2021-08-19 10:57
閱讀 1230·2021-08-17 10:14
閱讀 1487·2019-08-30 15:53
閱讀 1210·2019-08-30 15:43
閱讀 975·2019-08-30 13:16
閱讀 2983·2019-08-29 16:56