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)的文件描述符分三類(lèi):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返回后,需要輪詢(xún)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)閉)
?、诓l(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+'銷(xiāo)量', 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 +'銷(xiāo)量統(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 面膜
面膜銷(xiāo)量 1200
面膜銷(xiāo)量 1500
面膜銷(xiāo)量 3000
面膜銷(xiāo)量 None
面膜銷(xiāo)量統(tǒng)計(jì)完成??!
start key 手機(jī)
手機(jī)銷(xiāo)量 88
手機(jī)銷(xiāo)量 100
手機(jī)銷(xiāo)量 98
手機(jī)銷(xiāo)量 108
手機(jī)銷(xiāo)量 None
手機(jī)銷(xiāo)量統(tǒng)計(jì)完成??!
start key 衣服
衣服銷(xiāo)量 280
衣服銷(xiāo)量 560
衣服銷(xiāo)量 778
衣服銷(xiāo)量 70
衣服銷(xiāo)量 None
衣服銷(xiāo)量統(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ù)雜化了。看以下直接用main()函數(shù)直接去調(diào)用sales_sum代碼:
def sales_sum(pro_name):
total = 0
nums = []
while True:
x = yield
print(pro_name+'銷(xiāo)量', 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é)果:
面膜銷(xiāo)量 1200
面膜銷(xiāo)量 1500
面膜銷(xiāo)量 3000
面膜銷(xiāo)量 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 csv實(shí)時(shí)一條一條插入且表頭不重復(fù)問(wèn)題
這篇文章主要介紹了python csv實(shí)時(shí)一條一條插入且表頭不重復(fù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05
解讀pandas交叉表與透視表pd.crosstab()和pd.pivot_table()函數(shù)
這篇文章主要介紹了pandas交叉表與透視表pd.crosstab()和pd.pivot_table()函數(shù)的用法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09
Python3批量創(chuàng)建Crowd用戶并分配組
這篇文章主要介紹了Python3批量創(chuàng)建Crowd用戶并分配組,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05
Python+AutoIt實(shí)現(xiàn)界面工具開(kāi)發(fā)過(guò)程詳解
這篇文章主要介紹了Python+AutoIt實(shí)現(xiàn)界面工具開(kāi)發(fā)過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08
深度學(xué)習(xí)Tensorflow2.8實(shí)現(xiàn)GRU文本生成任務(wù)詳解
Python中Thop庫(kù)的基本用例和參數(shù)說(shuō)明

