這裏先引用一下百度百科的定義.html
併發,在操做系統中,是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行node
裏面的一個時間段內說明很是重要,這裏假設這個時間段是一秒,因此本文指的併發是指服務器在一秒中處理的請求數量,即rps,那麼rps高,本文就認爲高併發.python
啥?這不是你認爲的高併發, 出門左轉。git
若是由筆者來歸納,操做系統大概作了兩件事情,計算與IO,任何具體數學計算或者邏輯判斷,或者業務邏輯都是計算,而網絡交互,磁盤交互,人機之間的交互都是IO。github
根據筆者經驗,大多數時候在IO上面。注意,這裏說得是大多數,不是說絕對。golang
由於大多數時候業務本質上都是從數據庫或者其餘存儲上讀取內容,而後根據必定的邏輯,將數據返回給用戶,好比大多數web內容。而大多數邏輯的交互都算不上計算量多大的邏輯,CPU的速度要遠遠高於內存IO,磁盤IO,網絡IO, 而這些IO中網絡IO最慢。web
在根據上面的筆者對操做系統的概述,當併發高到必定的程度,根據業務的不一樣,好比計算密集,IO密集,或二者皆有,所以瓶頸可能出在計算上面或者IO上面,又或二者兼有。數據庫
而本文解決的高併發,是指IO密集的高併發瓶頸,所以,計算密集的高併發並不在本文的討論範圍內。編程
爲了使本文歧義更少,這裏的IO主要指網絡IO.flask
使用協程, 事件循環, 高效IO模型(好比多路複用,好比epoll), 三者缺一不可。
不少時候,筆者看過的文章都是說協程如何如何,最後告訴我一些協程庫或者asyncio用來講明協程的威力,最終我看懂了協程,卻仍是不知道它爲啥能高併發,這也是筆者寫本文的目的。
可是一切仍是得從生成器提及,由於asyncio或者大多數協程庫內部也是經過生成器實現的。
注意上面的三者缺一不可。
若是隻懂其中一個,那麼你懂了三分之一,以此類推,只有都會了,你才知道爲啥協程能高併發。
生成器的定義很抽象,如今不懂不要緊,可是當你懂了以後回過頭再看,會以爲定義的沒錯,而且準確。下面是定義
摘自百度百科: 生成器是一次生成一個值的特殊類型函數。能夠將其視爲可恢復函數。
關於生成器的內容,本文着重於生成器實現了哪些功能,而不是生成器的原理及內部實現。
簡單例子以下
def gen_func(): yield 1 yield 2 yield 3 if __name__ == '__main__': gen = gen_func() for i in gen: print(i) output: 1 2 3
上面的例子沒有什麼稀奇的不是嗎?yield像一個特殊的關鍵字,將函數變成了一個相似於迭代器的對象,可使用for循環取值。
協程天然不會這麼簡單,python協程的目標是星辰大海,從上面的例之因此get不到它的野心,是由於你沒有試過send, next兩個函數。
首先說next
def gen_func(): yield 1 yield 2 yield 3 if __name__ == '__main__': gen = gen_func() print(next(gen)) print(next(gen)) print(next(gen)) output: 1 2 3
next的操做有點像for循環,每調用一次next,就會從中取出一個yield出來的值,其實仍是沒啥特別的,感受尚未for循環好用。
不過,不知道你有沒有想過,若是你只須要一個值,你next一次就能夠了,而後你能夠去作其餘事情,等到須要的時候纔回來再次next取值。
就這一部分而言,你也許知道爲啥說生成器是能夠暫停的了,不過,這彷佛也沒什麼用,那是由於你不知到時,生成器除了能夠拋出值,還能將值傳遞進去。
接下來咱們看send的例子。
def gen_func(): a = yield 1 print("a: ", a) b = yield 2 print("b: ", b) c = yield 3 print("c: ", c) return "finish" if __name__ == '__main__': gen = gen_func() for i in range(4): if i == 0: print(gen.send(None)) else: # 由於gen生成器裏面只有三個yield,那麼只能循環三次。 # 第四次循環的時候,生成器會拋出StopIteration異常,而且return語句裏面內容放在StopIteration異常裏面 try: print(gen.send(i)) except StopIteration as e: print("e: ", e) output: 1 a: 1 2 b: 2 3 c: 3 e: finish
send有着next差很少的功能,不過send在傳遞一個值給生成器的同時,還能獲取到生成器yield拋出的值,在上面的代碼中,send分別將None,1,2,3四個值傳遞給了生成器,之因此第一須要傳遞None給生成器,是由於規定,之因此規定,由於第一次傳遞過去的值沒有特定的變量或者說對象能接收,因此規定只能傳遞None, 若是你傳遞一個非None的值進去,會拋出一下錯誤
TypeError: can't send non-None value to a just-started generator
從上面的例子咱們也發現,生成器裏面的變量a,b,c得到了,send函數發送未來的1, 2, 3.
若是你有事件循環或者說多路複用的經驗,你也許可以隱隱察覺到微妙的感受。
這個微妙的感受是,是否能夠將IO操做yield出來?由事件循環調度, 若是你能get到這個微妙的感受,那麼你已經知道協程高併發的祕密了.
可是還差一點點.嗯, 還差一點點了.
下面是yield from的例子
def gen_func(): a = yield 1 print("a: ", a) b = yield 2 print("b: ", b) c = yield 3 print("c: ", c) return 4 def middle(): gen = gen_func() ret = yield from gen print("ret: ", ret) return "middle Exception" def main(): mid = middle() for i in range(4): if i == 0: print(mid.send(None)) else: try: print(mid.send(i)) except StopIteration as e: print("e: ", e) if __name__ == '__main__': main() output: 1 a: 1 2 b: 2 3 c: 3 ret: 4 e: middle Exception
從上面的代碼咱們發現,main函數調用的middle函數的send,可是gen_func函數卻能接收到main函數傳遞的值.有一種透傳的感受,這就是yield from的做用, 這很關鍵。
而yield from最終傳遞出來的值是StopIteration異常,異常裏面的內容是最終接收生成器(本示例是gen_func)return出來的值,因此ret得到了gen_func函數return的4.可是ret將異常裏面的值取出以後會繼續將接收到的異常往上拋,因此main函數裏面須要使用try語句捕獲異常。而gen_func拋出的異常裏面的值已經被middle函數接收,因此middle函數會將拋出的異常裏面的值設爲自身return的值,
至今生成器的所有內容講解完畢,若是,你get到了這些功能,那麼你已經會使用生成器了。
再次強調,本小結只是說明生成器的功能,至於具體生成器內部怎麼實現的,你能夠去看其餘文章,或者閱讀源代碼.
Linux平臺一共有五大IO模型,每一個模型有本身的優勢與肯定。根據應用場景的不一樣可使用不一樣的IO模型。
不過本文主要的考慮場景是高併發,因此會針對高併發的場景作出評價。
同步模型天然是效率最低的模型了,每次只能處理完一個鏈接才能處理下一個,若是只有一個線程的話, 若是有一個鏈接一直佔用,那麼後來者只能傻傻的等了。因此不適合高併發,不過最簡單,符合慣性思惟。
不會阻塞後面的代碼,可是須要不停的顯式詢問內核數據是否準備好,通常經過while循環,而while循環會耗費大量的CPU。因此也不適合高併發。
當前最流行,使用最普遍的高併發方案。
而多路複用又有三種實現方式, 分別是select, poll, epoll。
select,poll因爲設計的問題,當處理鏈接過多會形成性能線性降低,而epoll是在前人的經驗上作過改進的解決方案。不會有此問題。
不過select, poll並非一無可取,假設場景是鏈接數很少,而且每一個鏈接很是活躍,select,poll是要性能高於epoll的。
至於爲啥,查看小結參考連接, 或者自行查詢資料。
可是本文講解的高併發但是指的鏈接數很是多的。
很偏門的一個IO模型,未曾碰見過使用案例。看模型也不見得比多路複用好用。
用得不是不少,理論上比多路複用更快,由於少了一次調用,可是實際使用並無比多路複用快很是多,因此爲啥不使用普遍使用的多路複用。
使用最普遍多路複用epoll, 可使得IO操做更有效率。可是使用上有必定的難度。
至此,若是你理解了多路複用的IO模型,那麼你瞭解python爲何可以經過協程實現高併發的三分之二了。
IO模型參考: https://www.jianshu.com/p/486b0965c296
select,poll,epoll區別參考: http://www.javashuo.com/article/p-txmndpyl-cm.html
上面的IO模型可以解決IO的效率問題,可是實際使用起來須要一個事件循環驅動協程去處理IO。
下面引用官方的一個簡單例子。
import selectors import socket # 建立一個selctor對象 # 在不一樣的平臺會使用不一樣的IO模型,好比Linux使用epoll, windows使用select(不肯定) # 使用select調度IO sel = selectors.DefaultSelector() # 回調函數,用於接收新鏈接 def accept(sock, mask): conn, addr = sock.accept() # Should be ready print('accepted', conn, 'from', addr) conn.setblocking(False) sel.register(conn, selectors.EVENT_READ, read) # 回調函數,用戶讀取client用戶數據 def read(conn, mask): data = conn.recv(1000) # Should be ready if data: print('echoing', repr(data), 'to', conn) conn.send(data) # Hope it won't block else: print('closing', conn) sel.unregister(conn) conn.close() # 建立一個非堵塞的socket sock = socket.socket() sock.bind(('localhost', 1234)) sock.listen(100) sock.setblocking(False) sel.register(sock, selectors.EVENT_READ, accept) # 一個事件循環,用於IO調度 # 當IO可讀或者可寫的時候, 執行事件所對應的回調函數 def loop(): while True: events = sel.select() for key, mask in events: callback = key.data callback(key.fileobj, mask) if __name__ == '__main__': loop()
上面代碼中loop函數對應事件循環,它要作的就是一遍一遍的等待IO,而後調用事件的回調函數.
可是做爲事件循環遠遠不夠,好比怎麼中止,怎麼在事件循環中加入其餘邏輯.
若是就功能而言,上面的代碼彷佛已經完成了高併發的影子,可是如你所見,直接使用select的編碼難度比較大, 再者回調函數素來有"回調地獄"的惡名.
實際生活中的問題要複雜的多,做爲一個調庫狂魔,怎麼可能會本身去實現這些,因此python官方實現了一個跨平臺的事件循環,至於IO模型具體選擇,官方會作適配處理。
不過官方實現是在Python3.5及之後了,3.5以前的版本只能使用第三方實現的高併發異步IO解決方案, 好比tornado,gevent,twisted。
至此你須要get到python高併發的必要條件了.
在本文開頭,筆者就說過,python要完成高併發須要協程,事件循環,高效IO模型.而Python自帶的asyncio模塊已經所有完成了.盡情使用吧.
下面是有引用官方的一個例子
import asyncio # 經過async聲明一個協程 async def handle_echo(reader, writer): # 將須要io的函數使用await等待, 那麼此函數就會中止 # 當IO操做完成會喚醒這個協程 # 能夠將await理解爲yield from data = await reader.read(100) message = data.decode() addr = writer.get_extra_info('peername') print("Received %r from %r" % (message, addr)) print("Send: %r" % message) writer.write(data) await writer.drain() print("Close the client socket") writer.close() # 建立事件循環 loop = asyncio.get_event_loop() # 經過asyncio.start_server方法建立一個協程 coro = asyncio.start_server(handle_echo, '127.0.0.1', 8888, loop=loop) server = loop.run_until_complete(coro) # Serve requests until Ctrl+C is pressed print('Serving on {}'.format(server.sockets[0].getsockname())) try: loop.run_forever() except KeyboardInterrupt: pass # Close the server server.close() loop.run_until_complete(server.wait_closed()) loop.close()
總的來講python3.5明確了什麼是協程,什麼是生成器,雖然原理差很少,可是這樣會使得不會讓生成器便可以做爲生成器使用(好比迭代數據)又能夠做爲協程。
因此引入了async,await使得協程的語義更加明確。
asyncio官方只實現了比較底層的協議,好比TCP,UDP。因此諸如HTTP協議之類都須要藉助第三方庫,好比aiohttp。
雖然異步編程的生態不夠同步編程的生態那麼強大,可是若是又高併發的需求不妨試試,下面說一下比較成熟的異步庫
異步http client/server框架
github地址: https://github.com/aio-libs/aiohttp
速度更快的類flask web框架。
github地址:
https://github.com/channelcat/sanic
快速,內嵌於asyncio事件循環的庫,使用cython基於libuv實現。
官方性能測試:
nodejs的兩倍,追平golang
github地址: https://github.com/MagicStack/uvloop
爲了減小歧義,這裏的性能測試應該只是網絡IO高併發方面不是說任何方面追平golang。
Python之因此可以處理網絡IO高併發,是由於藉助了高效的IO模型,可以最大限度的調度IO,而後事件循環使用協程處理IO,協程遇到IO操做就將控制權拋出,那麼在IO準備好以前的這段事件,事件循環就可使用其餘的協程處理其餘事情,而後協程在用戶空間,而且是單線程的,因此不會像多線程,多進程那樣頻繁的上下文切換,於是可以節省大量的沒必要要性能損失。
注: 不要再協程裏面使用time.sleep之類的同步操做,由於協程再單線程裏面,因此會使得整個線程停下來等待,也就沒有協程的優點了
本文主要講解Python爲何可以處理高併發,不是爲了講解某個庫怎麼使用,因此使用細節請查閱官方文檔或者執行。
不管什麼編程語言,高性能框架,通常由事件循環 + 高性能IO模型(也許是epoll)組成。