python 如何引入?yún)f(xié)程和原理分析
相關(guān)概念
- 并發(fā):指一個(gè)時(shí)間段內(nèi),有幾個(gè)程序在同一個(gè)cpu上運(yùn)行,但是任意時(shí)刻只有一個(gè)程序在cpu上運(yùn)行。比如說(shuō)在一秒內(nèi)cpu切換了100個(gè)進(jìn)程,就可以認(rèn)為cpu的并發(fā)是100。
- 并行:值任意時(shí)刻點(diǎn)上,有多個(gè)程序同時(shí)運(yùn)行在cpu上,可以理解為多個(gè)cpu,每個(gè)cpu獨(dú)立運(yùn)行自己程序,互不干擾。并行數(shù)量和cpu數(shù)量是一致的。
我們平時(shí)常說(shuō)的高并發(fā)而不是高并行,是因?yàn)閏pu的數(shù)量是有限的,不可以增加。
形象的理解:cpu對(duì)應(yīng)一個(gè)人,程序?qū)?yīng)喝茶,人要喝茶需要四個(gè)步驟(可以對(duì)應(yīng)程序需要開(kāi)啟四個(gè)線程):1燒水,2備茶葉,3洗茶杯,4泡茶。
并發(fā)方式:燒水的同時(shí)做好2備茶葉,3洗茶杯,等水燒好之后執(zhí)行4泡茶。這樣比順序執(zhí)行1234要省時(shí)間。
并行方式:叫來(lái)四個(gè)人(開(kāi)啟四個(gè)進(jìn)程),分別執(zhí)行任務(wù)1234,整個(gè)程序執(zhí)行時(shí)間取決于耗時(shí)最多的步驟。
- 同步 (注意同步和異步只是針對(duì)于I/O操作來(lái)講的)值調(diào)用IO操作時(shí),必須等待IO操作完成后才開(kāi)始新的的調(diào)用方式。
- 異步 指調(diào)用IO操作時(shí),不必等待IO操作完成就開(kāi)始新的的調(diào)用方式。
- 阻塞 指調(diào)用函數(shù)的時(shí)候,當(dāng)前線程被掛起。
- 非阻塞 指調(diào)用函數(shù)的時(shí)候,當(dāng)前線程不會(huì)被掛起,而是立即返回。
IO多路復(fù)用
sllect, poll, epoll都是IO多路復(fù)用的機(jī)制。IO多路復(fù)用就是通過(guò)這樣一種機(jī)制:一個(gè)進(jìn)程可以監(jiān)聽(tīng)多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒和寫(xiě)就緒),能夠通知程序進(jìn)行相應(yīng)的操作。但select,poll,epoll本質(zhì)上都是同步IO,因?yàn)樗麄兌夹枰谧x寫(xiě)事件就緒后自己負(fù)責(zé)進(jìn)行讀寫(xiě)(即將數(shù)據(jù)從內(nèi)核空間拷貝到應(yīng)用緩存)。也就是說(shuō)這個(gè)讀寫(xiě)過(guò)程是阻塞的。而異步IO則無(wú)需自己負(fù)責(zé)讀寫(xiě),異步IO的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
select
select函數(shù)監(jiān)聽(tīng)的文件描述符分三類:writefds、readfds、和exceptfds。調(diào)用后select函數(shù)會(huì)阻塞,直到描述符就緒(有數(shù)據(jù)可讀、寫(xiě)、或者有except)或者超時(shí)(timeout指定等待時(shí)間,如果立即返回則設(shè)置為null),函數(shù)返回。當(dāng)select函數(shù)返回后,可以通過(guò)遍歷fdset,來(lái)找到就緒的描述符。
優(yōu)點(diǎn):良好的跨平臺(tái)性(幾乎所有的平臺(tái)都支持)
缺點(diǎn):?jiǎn)蝹€(gè)進(jìn)程能夠監(jiān)聽(tīng)的文件描述符數(shù)量存在最大限制,在linux上一般為1024,可以通過(guò)修改宏定義甚至重新編譯內(nèi)核來(lái)提升,但是這樣也會(huì)造成效率降低。
poll
不同于select使用三個(gè)位圖來(lái)表示fdset的方式,poll使用的是pollfd的指針實(shí)現(xiàn)
pollfd結(jié)構(gòu)包含了要監(jiān)聽(tīng)的event和發(fā)生的event,不再使用select“參數(shù)-值”傳遞的方式。同時(shí)pollfd并沒(méi)有最大數(shù)量限制(但是數(shù)量過(guò)大之后性能也是會(huì)下降)。和select函數(shù)一樣,poll返回后,需要輪詢pollfd來(lái)獲取就緒的描述符。
從上面看,select和poll都需要在返回后,通過(guò)遍歷文件描述符來(lái)獲取已經(jīng)就緒的socket。事實(shí)上,同時(shí)連接的大量客戶端在同一時(shí)刻可能只有很少的處于就緒狀態(tài),因此隨著監(jiān)視的描述符數(shù)量的增長(zhǎng),其效率也會(huì)下降。
epoll
epoll是在linux2.6內(nèi)核中國(guó)提出的,(windows不支持),是之前的select和poll增強(qiáng)版。相對(duì)于select和poll來(lái)說(shuō),epoll更加靈活,沒(méi)有描述符的限制。epoll使用一個(gè)文件描述符管理多個(gè)描述符,將用戶關(guān)系的文件描述符的時(shí)間存放到內(nèi)核的一個(gè)時(shí)間表中。這樣在用戶控件和內(nèi)核控件的coppy只需要一次。
如何選擇?
?、僭诓l(fā)高同時(shí)連接活躍度不是很高的請(qǐng)看下,epoll比select好(網(wǎng)站或web系統(tǒng)中,用戶請(qǐng)求一個(gè)頁(yè)面后隨時(shí)可能會(huì)關(guān)閉)
②并發(fā)性不高,同時(shí)連接很活躍,select比epoll好。(比如說(shuō)游戲中數(shù)據(jù)一但連接了就會(huì)一直活躍,不會(huì)中斷)
省略章節(jié):由于在用到select的時(shí)候需要嵌套多層回調(diào)函數(shù),然后印發(fā)一系列的問(wèn)題,如可讀性差,共享狀態(tài)管理困難,出現(xiàn)異常排查復(fù)雜,于是引入?yún)f(xié)程,既操作簡(jiǎn)單,速度又快。
協(xié)程
對(duì)于上面的問(wèn)題,我們希望去解決這樣幾個(gè)問(wèn)題:
- 采用同步的方式去編寫(xiě)異步的代碼,使代碼的可讀性高,更簡(jiǎn)便。
- 使用單線程去切換任務(wù)(就像單線程間函數(shù)之間的切換那樣,速度超快)
?。?)線程是由操作系統(tǒng)切換的,單線程的切換意味著我們需要程序員自己去調(diào)度任務(wù)。
(2)不需要鎖,并發(fā)性高,如果單線程內(nèi)切換函數(shù),性能遠(yuǎn)高于線程切換,并發(fā)性更高。
例如我們?cè)谧雠老x(chóng)的時(shí)候:
def get_url(url): html = get_html(url) # 此處網(wǎng)絡(luò)下載IO操作比較耗時(shí),希望切換到另一個(gè)函數(shù)去執(zhí)行 infos = parse_html(html) # 下載url中的html def get_html(url): pass # 解析網(wǎng)頁(yè) def parse_html(html): pass
意味著我們需要一個(gè)可以暫停的函數(shù),對(duì)于此函數(shù)可以向暫停的地方穿入值。(回憶我們的生成器函數(shù)就可以滿足這兩個(gè)條件)所以就引入了協(xié)程。
生成器進(jìn)階
- 生成器不僅可以產(chǎn)出值,還可以接收值,用send()方法。注意:在調(diào)用send()發(fā)送非None值之前必須先啟動(dòng)生成器,可以用①next()②send(None)兩種方式激活
def gen_func(): html = yield 'http://www.baidu.com' # yield 前面加=號(hào)就實(shí)現(xiàn)了1:可以產(chǎn)出值2:可以接受調(diào)用者傳過(guò)來(lái)的值 print(html) yield 2 yield 3 return 'bobby' if __name__ == '__main__': gen = gen_func() url = next(gen) print(url) html = 'bobby' gen.send(html) # send方法既可以將值傳遞進(jìn)生成器內(nèi)部,又可以重新啟動(dòng)生成器執(zhí)行到下一yield位置。 打印結(jié)果: http://www.baidu.com bobby
- close()方法。
def gen_func(): yield 'http://www.baidu.com' # yield 前面加=號(hào)就實(shí)現(xiàn)了1:可以產(chǎn)出值2:可以接受調(diào)用者傳過(guò)來(lái)的值 yield 2 yield 3 return 'bobby' if __name__ == '__main__': gen = gen_func() url = next(gen) gen.close() next(gen) 輸出結(jié)果: StopIteration
特別注意:調(diào)用close.()之后, 生成器在往下運(yùn)行的時(shí)候就會(huì)產(chǎn)生出一個(gè)GeneratorExit,單數(shù)如果用try捕獲異常的話,就算捕獲了遇到后面還有yield的話,還是不能往下運(yùn)行了,因?yàn)橐坏┱{(diào)用close方法生成器就終止運(yùn)行了(如果還有next,就會(huì)會(huì)產(chǎn)生一個(gè)異常)所以我們不要去try捕捉該異常。(此注意可以先忽略)
def gen_func(): try: yield 'http://www.baidu.com' except GeneratorExit: pass yield 2 yield 3 return 'bobby' if __name__ == '__main__': gen = gen_func() print(next(gen)) gen.close() next(gen) 輸出結(jié)果: RuntimeError: generator ignored GeneratorExit
- 調(diào)用throw()方法。用于拋出一個(gè)異常。該異??梢圆蹲胶雎?。
def gen_func(): yield 'http://www.baidu.com' # yield 前面加=號(hào)就實(shí)現(xiàn)了1:可以產(chǎn)出值2:可以接受調(diào)用者傳過(guò)來(lái)的值 yield 2 yield 3 return 'bobby' if __name__ == '__main__': gen = gen_func() print(next(gen)) gen.throw(Exception, 'Download Error') 輸出結(jié)果: Download Error
yield from
先看一個(gè)函數(shù):from itertools import chain
from itertools import chain my_list = [1,2,3] my_dict = {'frank':'yangchao', 'ailsa':'liuliu'} for value in chain(my_list, my_dict, range(5,10)): chain()方法可以傳入多個(gè)可迭代對(duì)象,然后分別遍歷之。 print(value) 打印結(jié)果: 1 2 3 frank ailsa 5 6 7 8 9
此函數(shù)可以用yield from 實(shí)現(xiàn):yield from功能 1:從一個(gè)可迭代對(duì)象中將值逐個(gè)返回。
my_list = [1,2,3] my_dict = {'frank':'yangchao', 'ailsa':'liuliu'} def chain(*args, **kwargs): for itemrable in args: yield from itemrable for value in chain(my_list, my_dict, range(5,10)): print(value)
看如下代碼:
def gen(): yield 1 def g1(gen): yield from gen def main(): g = g1(gen) g.send(None)
代碼分析:此代碼中main調(diào)用了g1, main就叫作調(diào)用方, g1叫做委托方, gen 叫做子生成器yield from將會(huì)在調(diào)用方main與子生成器gen之間建立一個(gè)雙向通道。(意味著可以直接越過(guò)委托方)
例子:當(dāng)委托方middle()中使用yield from 的時(shí)候,調(diào)用方main直接和子生成器sales_sum形成數(shù)據(jù)通道。
final_result = {} def sales_sum(pro_name): total = 0 nums = [] while True: x = yield print(pro_name+'銷量', x) if not x: break total += x nums.append(x) return total, nums #程序運(yùn)行到return的時(shí)候,會(huì)將return的返回值返回給委托方,即middle中的final_result[key] def middle(key): while True: #相當(dāng)于不停監(jiān)聽(tīng)sales_sum是否有返回?cái)?shù)據(jù),(本例中有三次返回) final_result[key] = yield from sales_sum(key) print(key +'銷量統(tǒng)計(jì)完成??!') def main(): data_sets = { '面膜':[1200, 1500, 3000], '手機(jī)':[88, 100, 98, 108], '衣服':[280, 560,778,70], } for key, data_set in data_sets.items(): print('start key', key) m = middle(key) m.send(None) # 預(yù)激生成器 for value in data_set: m.send(value) m.send(None)# 發(fā)送一個(gè)None使sales_sum中的x值為None退出while循環(huán) print(final_result) if __name__ == '__main__': main() 結(jié)果: start key 面膜 面膜銷量 1200 面膜銷量 1500 面膜銷量 3000 面膜銷量 None 面膜銷量統(tǒng)計(jì)完成!! start key 手機(jī) 手機(jī)銷量 88 手機(jī)銷量 100 手機(jī)銷量 98 手機(jī)銷量 108 手機(jī)銷量 None 手機(jī)銷量統(tǒng)計(jì)完成!! start key 衣服 衣服銷量 280 衣服銷量 560 衣服銷量 778 衣服銷量 70 衣服銷量 None 衣服銷量統(tǒng)計(jì)完成??! {'面膜': (5700, [1200, 1500, 3000]), '手機(jī)': (394, [88, 100, 98, 108]), '衣服': (1688, [280, 560, 778, 70])}
也許有人會(huì)好奇,為什么不能直接用main()函數(shù)直接去調(diào)用sales_sum呢?加一個(gè)委托方使代碼復(fù)雜化了??匆韵轮苯佑胢ain()函數(shù)直接去調(diào)用sales_sum代碼:
def sales_sum(pro_name): total = 0 nums = [] while True: x = yield print(pro_name+'銷量', x) if not x: break total += 1 nums.append(x) return total, nums if __name__ == '__main__': my_gen = sales_sum('面膜') my_gen.send(None) my_gen.send(1200) my_gen.send(1500) my_gen.send(3000) my_gen.send(None) 輸出結(jié)果: 面膜銷量 1200 面膜銷量 1500 面膜銷量 3000 面膜銷量 None Traceback (most recent call last): File "D:/MyCode/Cuiqingcai/Flask/test01.py", line 56, in <module> my_gen.send(None) StopIteration: (3, [1200, 1500, 3000])
從上述代碼可以看出,即使數(shù)據(jù)return結(jié)果出來(lái)了,還是會(huì)返回一個(gè)exception,由此可以看出yield from的一個(gè)最大優(yōu)點(diǎn)就是當(dāng)子生成器運(yùn)行時(shí)候出現(xiàn)異常,yield from可以直接自動(dòng)處理這些異常。
yield from 功能總結(jié):
子生成器生產(chǎn)的值,都是直接給調(diào)用方;調(diào)用發(fā)通過(guò).send()發(fā)送的值都是直接傳遞給子生成器,如果傳遞None,會(huì)調(diào)用子生成器的next()方法,如果不是None,會(huì)調(diào)用子生成器的sen()方法。
子生成器退出的時(shí)候,最后的return EXPR,會(huì)觸發(fā)一個(gè)StopIteration(EXPR)異常
yield from 表達(dá)式的值,是子生成器終止時(shí),傳遞給StopIteration異常的第一個(gè)參數(shù)。
如果調(diào)用的時(shí)候出現(xiàn)了StopIteration異常,委托方生成器恢復(fù)運(yùn)行,同時(shí)其他的異常向上冒泡。
傳入委托生成器的異常里,除了GeneratorExit之后,其他所有異常全部傳遞給子生成器的.throw()方法;如果調(diào)用.throw()的時(shí)候出現(xiàn)StopIteration異常,那么就恢復(fù)委托生成器的運(yùn)行,其他的異常全部向上冒泡
如果在委托生成器上調(diào)用.close()或傳入GeneratorExit異常,會(huì)調(diào)用子生成器的.close()方法,沒(méi)有就不調(diào)用,如果在調(diào)用.close()時(shí)候拋出了異常,那么就向上冒泡,否則的話委托生成器跑出GeneratorExit 異常。
以上就是python 如何引入?yún)f(xié)程和原理分析的詳細(xì)內(nèi)容,更多關(guān)于python 協(xié)程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python 去除二維數(shù)組/二維列表中的重復(fù)行方法
今天小編就為大家分享一篇python 去除二維數(shù)組/二維列表中的重復(fù)行方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-01-01基于Django框架利用Ajax實(shí)現(xiàn)點(diǎn)贊功能實(shí)例代碼
點(diǎn)贊這個(gè)功能是我們現(xiàn)在經(jīng)常會(huì)遇到的一個(gè)功能,下面這篇文章主要給大家介紹了關(guān)于基于Django框架利用Ajax實(shí)現(xiàn)點(diǎn)贊功能的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08在Python中預(yù)先初始化列表內(nèi)容和長(zhǎng)度的實(shí)現(xiàn)
今天小編就為大家分享一篇在Python中預(yù)先初始化列表內(nèi)容和長(zhǎng)度的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11python網(wǎng)絡(luò)爬蟲(chóng)基于selenium爬取斗魚(yú)直播信息
目前是直播行業(yè)的一個(gè)爆發(fā)期,由于國(guó)家對(duì)直播行業(yè)進(jìn)行整頓和規(guī)范,現(xiàn)在整個(gè)直播行業(yè)也在穩(wěn)固發(fā)展。隨著互聯(lián)網(wǎng)和網(wǎng)絡(luò)直播市場(chǎng)的快速發(fā)展,相信未來(lái)還有廣闊的發(fā)展前景。今天用selenium爬取一下斗魚(yú)直播信息將代碼分享給大家2022-03-03python數(shù)據(jù)分析工具之 matplotlib詳解
對(duì)于 Python 來(lái)說(shuō),matplotlib 是最著名的繪圖庫(kù),它主要用于二維繪圖,當(dāng)然也可以進(jìn)行簡(jiǎn)單的三維繪圖。這篇文章主要介紹了python數(shù)據(jù)分析工具之 matplotlib的相關(guān)知識(shí),需要的朋友可以參考下2020-04-04Python正則表達(dá)式使用經(jīng)典實(shí)例
本文給大家總結(jié)了17種python正則表達(dá)式使用經(jīng)典實(shí)例,非常不錯(cuò)具有參考借鑒價(jià)值,感興趣的朋友一起學(xué)習(xí)吧2016-06-06Python自制一個(gè)PDF轉(zhuǎn)PNG圖片小工具
這篇文章主要為大家詳細(xì)介紹了如何利用Python中的PyQt5自制一個(gè)PDF轉(zhuǎn)PNG格式圖片的小工具,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2023-02-02使用Python中的線程進(jìn)行網(wǎng)絡(luò)編程的入門教程
這篇文章主要介紹了使用Python中的線程進(jìn)行網(wǎng)絡(luò)編程的入門教程,本文來(lái)自于IBM官方網(wǎng)站技術(shù)文檔,需要的朋友可以參考下2015-04-04