Python也能高並發
點擊上方「
Python開發
」,選擇「置頂公眾號」
關鍵時刻,第一時間送達!
作者:youerning
鏈接:http://blog.51cto.com/youerning/2161196
Python開發整理髮布,轉載請聯繫作者獲得授權
前言
這裡先引用一下百度百科的定義.
並發,在操作系統中,是指一個時間段中有幾個程序都處於已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行
裡面的一個時間段內說明非常重要,這裡假設這個時間段是一秒,所以本文指的並發是指伺服器在一秒中處理的請求數量,即rps,那麼rps高,本文就認為高並發.
啥?這不是你認為的高並發, 出門左轉。
操作系統到底在幹啥?
如果由筆者來概括,操作系統大概做了兩件事情,計算與IO,任何具體數學計算或者邏輯判斷,或者業務邏輯都是計算,而網路交互,磁碟交互,人機之間的交互都是IO。
高並發的瓶頸在哪?
根據筆者經驗,
大多數
時候在IO上面。注意,這裡說得是大多數,不是說絕對。因為大多數時候業務本質上都是從資料庫或者其他存儲上讀取內容,然後根據一定的邏輯,將數據返回給用戶,比如
大多數
web內容。而大多數邏輯的交互都算不上計算量多大的邏輯,CPU的速度要遠遠高於內存IO,磁碟IO,網路IO, 而這些IO中網路IO最慢。在根據上面的筆者對操作系統的概述,當並發高到一定的程度,根據業務的不同,比如計算密集,IO密集,或兩者皆有,因此瓶頸可能出在計算上面或者IO上面,又或兩者兼有。
而本文解決的高並發,是指IO密集的高並發瓶頸,因此,
計算密集的高並發
並不在本文的討論範圍內。
為了使本文歧義更少,這裡的IO主要指
網路IO
.Python怎麼處理高並發?
使用協程, 事件循環, 高效IO模型(比如多路復用,比如epoll),
三者缺一不可
。很多時候,筆者看過的文章都是說協程如何如何,最後告訴我一些協程庫或者asyncio用來說明協程的威力,最終我看懂了協程,卻還是不知道它為啥能高並發,這也是筆者寫本文的目的。
但是一切還是得從生成器說起,因為asyncio或者大多數協程庫內部也是通過生成器實現的。
注意上面的
三者缺一不可
。如果只懂其中一個,那麼你懂了三分之一,以此類推,只有都會了,你才知道為啥協程能高並發。
生成器
生成器的定義很抽象,現在不懂沒關係,但是當你懂了之後回過頭再看,會覺得定義的沒錯,並且準確。下面是定義
摘自百度百科: 生成器是一次生成一個值的特殊類型函數。可以將其視為可恢複函數。
關於生成器的內容,本文著重於生成器實現了哪些功能,而不是生成器的原理及內部實現。
yield
簡單例子如下
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循環取值。
send, next
協程自然不會這麼簡單,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
下面是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到了這些功能,那麼你已經會使用生成器了。
小結
再次強調,本小結只是說明生成器的功能,至於具體生成器內部怎麼實現的,你可以去看其他文章,或者閱讀源代碼.
io模型
Linux平台一共有五大IO模型,每個模型有自己的優點與確定。根據應用場景的不同可以使用不同的IO模型。
不過本文主要的考慮場景是高並發,所以會
針對高並發的場景做出評價
。同步IO
同步模型自然是效率最低的模型了,每次只能處理完一個連接才能處理下一個,如果
只有一個線程的話
, 如果有一個連接一直佔用,那麼後來者只能傻傻的等了。所以不適合高並發,不過最簡單,符合慣性思維。非阻塞式IO
不會阻塞後面的代碼,但是需要不停的顯式詢問內核數據是否準備好,一般通過while循環,而while循環會耗費大量的CPU。所以也不適合高並發。
多路復用
當前最流行,使用最廣泛的高並發方案。
而多路復用又有三種實現方式, 分別是select, poll, epoll。
select, poll, epoll
select,poll由於設計的問題,當處理連接過多會造成性能線性下降,而epoll是在前人的經驗上做過改進的解決方案。不會有此問題。
不過select, poll並不是一無是處,假設場景是連接數不多,並且每個連接非常活躍,select,poll是要性能高於epoll的。
至於為啥,查看小結參考鏈接, 或者自行查詢資料。
但是本文講解的高並發可是指的連接數非常多的。
信號驅動式IO
很偏門的一個IO模型,不曾遇見過使用案例。看模型也不見得比多路復用好用。
非同步非阻塞IO
用得不是很多,理論上比多路復用更快,因為少了一次調用,但是實際使用並沒有比多路復用快非常多,所以為啥不使用廣泛使用的多路復用。
小結
使用最廣泛多路復用epoll, 可以使得IO操作更有效率。但是使用上有一定的難度。
至此,如果你理解了多路復用的IO模型,那麼你了解python為什麼能夠通過協程實現高並發的三分之二了。
IO模型參考: https://www.jianshu.com/p/486b0965c296
select,poll,epoll區別參考: https://www.cnblogs.com/Anker/p/3265058.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高並發的必要條件了.
asyncio
在本文開頭,筆者就說過,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生態
asyncio官方只實現了比較底層的協議,比如TCP,UDP。所以諸如HTTP協議之類都需要藉助第三方庫,比如aiohttp。
雖然非同步編程的生態不夠同步編程的生態那麼強大,但是如果又高並發的需求不妨試試,下面說一下比較成熟的非同步庫
aiohttp
非同步http client/server框架
github地址: https://github.com/aio-libs/aiohttp
sanic
速度更快的類flask web框架。
github地址:
https://github.com/channelcat/sanic
uvloop
快速,內嵌於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)組成。【點擊成為Java大神】
TAG:Python開發 |