Python也能高并發

      網友投稿 700 2025-03-31

      里面的一個時間段內說明非常重要,這里假設這個時間段是一秒,所以本文指的并發是指服務器在一秒中處理的請求數量,即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?3if?__name__?==?'__main__': ????gen?=?gen_func()????for?i?in?gen: ????????print(i) output:123

      上面的例子沒有什么稀奇的不是嗎?yield像一個特殊的關鍵字,將函數變成了一個類似于迭代器的對象,可以使用for循環取值。

      send, next

      協程自然不會這么簡單,python協程的目標是星辰大海,從上面的例之所以get不到它的野心,是因為你沒有試過send, next兩個函數。

      首先說next

      def?gen_func(): ????yield?1 ????yield?2 ????yield?3if?__name__?==?'__main__': ????gen?=?gen_func() ????print(next(gen)) ????print(next(gen)) ????print(next(gen)) output:123

      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:1a:??12b:??23c:??3e:??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?4def?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:? 1a:??12b:??23c:??3ret:??4e:??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

      同步模型自然是效率最低的模型了,每次只能處理完一個連接才能處理下一個,如果只有一個線程的話, 如果有一個連接一直占用,那么后來者只能傻傻的等了。所以不適合高并發,不過最簡單,符合慣性思維。

      Python也能高并發

      非阻塞式IO

      不會阻塞后面的代碼,但是需要不停的顯式詢問內核數據是否準備好,一般通過while循環,而while循環會耗費大量的CPU。所以也不適合高并發。

      多路復用

      當前最流行,使用最廣泛的高并發方案。

      而多路復用又有三種實現方式, 分別是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?selectorsimport?socket#?創建一個selctor對象#?在不同的平臺會使用不同的IO模型,比如Linux使用epoll,?windows使用select(不確定)#?使用select調度IOsel?=?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()#?創建一個非堵塞的socketsock?=?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?pressedprint('Serving?on?{}'.format(server.sockets[0].getsockname())) try:????loop.run_forever() except?KeyboardInterrupt: ????pass#?Close?the?serverserver.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)組成。

      編程語言 python

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:訂單處理業務的流程(訂單處理業務的流程是什么)
      下一篇:表格軟件excel 編號(Excel中編號)
      相關文章
      亚洲AV色吊丝无码| 亚洲精品国产成人| 伊人久久五月丁香综合中文亚洲 | 国产成人综合亚洲| 亚洲精品动漫免费二区| 亚洲精品成a人在线观看☆| 亚洲精品无码专区| 亚洲国产精品成人综合色在线| 亚洲欧美日韩中文无线码| 亚洲国产综合AV在线观看| 亚洲国产成人精品无码区二本| 亚洲成a人无码亚洲成av无码 | 亚洲欧美熟妇综合久久久久| 亚洲欧美国产日韩av野草社区| 亚洲国产综合AV在线观看| 在线观看亚洲专区| 亚洲精品成人a在线观看| 亚洲国产成人精品女人久久久 | 亚洲av无码天堂一区二区三区 | 色天使亚洲综合在线观看| 亚洲欧美日本韩国| 久久精品熟女亚洲av麻豆| 亚洲国产成人爱av在线播放| 亚洲中文字幕在线第六区| 亚洲精品午夜无码专区| 亚洲成在人天堂一区二区| 亚洲综合激情另类小说区| 亚洲一级片在线观看| 亚洲色大成网站WWW国产| 精品久久久久久亚洲综合网| 亚洲欧洲久久av| 久久亚洲高清观看| 久久狠狠高潮亚洲精品| 亚洲videosbestsex日本| 亚洲欧美国产欧美色欲| 亚洲色一色噜一噜噜噜| 亚洲精品国产精品乱码不99| 久久丫精品国产亚洲av不卡| 中文字幕 亚洲 有码 在线| 自拍偷自拍亚洲精品播放| 毛茸茸bbw亚洲人|