(見右側目錄欄導航)
python
- 1. 前言
- 2. IO的五種模型
- 3. 協程
- 3.1 協程的概念
- 4. Gevent 模塊
- 4.1 gevent 基本使用
- 4.2 gevent應用一:爬蟲
- 4.3 gevent應用二:網絡編程
編程
CPU的速度遠遠快於磁盤、網絡等IO。在一個線程中,CPU執行代碼的速度極快,然而,一旦遇到IO操做,如讀寫文件、發送網絡數據時,就須要等待IO操做完成,才能繼續進行下一步操做。這種狀況稱爲同步IO。在IO操做的過程當中,當前線程被掛起,而其餘須要CPU執行的代碼就沒法被當前線程執行了。由於一個IO操做就阻塞了當前線程,致使其餘代碼沒法執行,因此咱們必須使用多線程或者多進程來併發執行代碼,爲多個用戶服務。每一個用戶都會分配一個線程,若是遇到IO致使線程被掛起,其餘用戶的線程不受影響。多線程和多進程的模型雖然解決了併發問題,可是系統不能無上限地增長線程。因爲系統切換線程的開銷也很大,因此,一旦線程數量過多,CPU的時間就花在線程切換上了,真正運行代碼的時間就少了,結果致使性能嚴重降低。因爲咱們要解決的問題是CPU高速執行能力和IO設備的龜速嚴重不匹配,多線程和多進程只是解決這一問題的一種方法,另外一種解決IO問題的方法是異步IO。當代碼須要執行一個耗時的IO操做時,它只發出IO指令,並不等待IO結果,而後就去執行其餘代碼了。一段時間後,當IO返回結果時,再通知CPU進行處理。網絡
(1)blocking IO (阻塞IO)多線程
(2)noblocking IO (非阻塞IO)併發
(3)IO multiplexing (IO多路複用)異步
(4)signal driven IO(信號驅動IO) -- 不經常使用socket
(5)asynchronous IO (異步IO)async
在理解上面五種IO模式以前須要理解如下4個概念:ide
同步、異步、阻塞、非阻塞異步編程
2.1 同步和異步
同步和異步關注的是消息通訊機制
同步:在發出一個調用時,沒獲得結果以前,該調用就不返回。可是一旦調用返回就獲得返回值(結果)了,調用者須要主動等待這個調用的結果。
異步:在發送一個調用時,這個調用就直接返回了,無論返回有沒有結果。當一個異步過程調用發出後,被調用者經過狀態,通知調用者,或者經過回調函數處理這個調用
2.2 阻塞和非阻塞
阻塞和非阻塞關注的是程序在等待調用結果時的狀態
阻塞:調用結果返回以前,當前線程會被掛起。調用線程只有在獲得結果以後才返回;
非阻塞:在不能當即獲得結果以前,該調用不會掛起當前線程
有一個很好的例子說明這4者之間的關係:
老張愛喝茶,廢話不說,煮開水。 出場人物:老張,水壺兩把(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺)。
1 老張把水壺放到火上,立等水開。(同步阻塞) 老張以爲本身有點傻
2 老張把水壺放到火上,去客廳看電視,時不時去廚房看看水開沒有。(同步非阻塞) 老張仍是以爲本身有點傻,因而變高端了,買了把會響笛的那種水壺。水開以後,能大聲發出嘀~~~~的噪音。
3 老張把響水壺放到火上,立等水開。(異步阻塞) 老張以爲這樣傻等意義不大
4 老張把響水壺放到火上,去客廳看電視,水壺響以前再也不去看它了,響了再去拿壺。(異步非阻塞) 老張以爲本身聰明瞭。
所謂同步異步,只是對於水壺而言。 普通水壺,同步;響水壺,異步。 雖然都能幹活,但響水壺能夠在本身完工以後,提示老張水開了。這是普通水壺所不能及的。 同步只能讓調用者去輪詢本身(狀況2中),形成老張效率的低下。
所謂阻塞非阻塞,僅僅對於老張而言。 立等的老張,阻塞;看電視的老張,非阻塞。 狀況1和狀況3中老張就是阻塞的,媳婦喊他都不知道。雖然3中響水壺是異步的,可對於立等的老張沒有太大的意義。因此通常異步是配合非阻塞使用的,這樣才能發揮異步的效用。
3.1 協程的概念
進程是資源分配的最小單位,線程是CPU調度的基本單位, 在Cpython中,因爲GIL鎖的存在,通常來講,同一時間片只有一個線程在cpu中運行,爲了提升單線程的效率,這裏提出了協程的概念。
協程:是單線程下的併發,又稱微線程,纖程。英文名Coroutine。一句話說明什麼是協程:協程是一種用戶態的輕量級線程,即協程是由用戶程序本身控制調度的。
須要強調:
1. python的線程屬於內核級別的,即由操做系統控制調度(如單線程遇到io或執行時間過長就會被迫交出cpu執行權限,切換其餘線程運行)
2. 單線程內開啓協程,一旦遇到io,就會從應用程序級別(而非操做系統)控制切換,以此來提高效率(!!!非io操做的切換與效率無關)
對比操做系統控制線程的切換,用戶在單線程內控制協程的切換
優勢以下:
1. 協程的切換開銷更小,屬於程序級別的切換,操做系統徹底感知不到,於是更加輕量級
2. 單線程內就能夠實現併發的效果,最大限度地利用cpu
缺點以下:
1. 協程的本質是單線程下,沒法利用多核,能夠是一個程序開啓多個進程,每一個進程內開啓多個線程,每一個線程內開啓協程
2. 協程指的是單個線程,於是一旦協程出現阻塞,將會阻塞整個線程
總結協程的特色:
1. 必須在只有一個單線程裏實現併發
2. 修改共享數據不需加鎖
3. 用戶程序裏本身保存多個控制流的上下文棧
4. 一個協程遇到IO操做自動切換到其餘協程
4.1 gevent 基本使用
Gevent 是一個第三方庫,能夠輕鬆經過gevent實現併發同步或異步編程。
g1=gevent.spawn(func,1,,2,3,x=4,y=5) 建立一個協程對象g1,spawn括號內第一個參數是函數名,如eat,後面能夠有多個參數,能夠是位置實參或關鍵字實參,都是傳給函數eat的 g2=gevent.spawn(func2) g1.join() 等待g1結束 g2.join() 等待g2結束 或者上述兩步合做一步: gevent.joinall([g1,g2]) g1.value 拿到func1的返回值
使用gevent 遇到IO就切換實例:
import gevent def eat(): print('eat start...') gevent.sleep(2) print('eat end.') def play(): print('play start...') gevent.sleep(2) print('play end.') if __name__ == '__main__': g1 = gevent.spawn(eat) g2 = gevent.spawn(play) g1.join() g2.join() print('----主-----')
上例gevent.sleep(2)模擬的是gevent能夠識別的io阻塞,而time.sleep(1)或其餘的阻塞,gevent是不能直接識別的須要用下面一行代碼,打補丁,就能夠識別了
from gevent import monkey;monkey.patch_all()必須放到被打補丁者的前面,如time,socket模塊以前或者咱們乾脆記憶成:要用gevent,須要將from gevent import monkey;monkey.patch_all()放到文件的開頭
from gevent import monkey; monkey.patch_all() import gevent import time def eat(): print('eat start...') time.sleep(2) print('eat end.') def play(): print('play start...') time.sleep(2) print('play end.') if __name__ == '__main__': g1 = gevent.spawn(eat) g2 = gevent.spawn(play) g1.join() g2.join() print('----主-----')
咱們能夠用threading.current_thread().getName()來查看每一個g1和g2,查看的結果爲DummyThread-n,即假線程
from gevent import monkey; monkey.patch_all() import threading import gevent import time def eat(): print(threading.current_thread().name) print('eat start...') time.sleep(2) print('eat end.') def play(): print(threading.current_thread().name) print('play start...') time.sleep(2) print('play end.') if __name__ == '__main__': g1 = gevent.spawn(eat) g2 = gevent.spawn(play) g1.join() g2.join() print('----主-----') 執行結果: DummyThread-1 eat start... DummyThread-2 play start... (阻塞2秒) eat end. play end. ----主-----
4.2 gevent 應用一:爬蟲
from gevent import monkey; monkey.patch_all() import gevent import requests def get(url): print('GET:', url) response = requests.get(url) if response.status_code == 200: print('%d bytes recevied from %s' % (len(response.text), url)) if __name__ == '__main__': gevent.joinall([ gevent.spawn(get, 'https://www.baidu.com'), gevent.spawn(get, 'https://www.taobao.com'), gevent.spawn(get, 'https://www.jd.com')])
4.3 gevent 應用二:網絡編程
經過gevent實現單線程下的socket併發
注意:from gevent import monkey;monkey.patch_all()必定要放到導入socket模塊以前,不然gevent沒法識別socket的阻塞
from gevent import spawn, monkey;monkey.patch_all() import socket def server(ip_port): sk_server = socket.socket() sk_server.bind(ip_port) sk_server.listen(5) while True: conn, addr = sk_server.accept() spawn(walk, conn) def walk(conn): conn.send(b'welcome!') try: while True: res = conn.recv(1024) print(res) conn.send(res.upper()) except Exception as e: print(e) finally: conn.close() if __name__ == '__main__': server(('localhost', 8080))
import socket sk_client = socket.socket() sk_client.connect(('localhost', 8080)) res = sk_client.recv(1024) print(res) while True: inp = input('>>>').strip() if not inp: continue sk_client.send(inp.encode()) print(sk_client.recv(1024))