協程 協程又稱為微線程,協程是一種用戶態的輕量級線程 協程擁有自己的寄存器和棧。協程調度切換的時候,將寄存器上下文和棧都保存到其他地方,在切換回來的時候,恢復到先前保存的寄存器上下文和棧,因此:協程能保留上一次調用狀態,每次過程重入時,就相當於進入上一次調用的狀態。 協程的好處: 1.無需線程上下文 ...
協程
協程又稱為微線程,協程是一種用戶態的輕量級線程
協程擁有自己的寄存器和棧。協程調度切換的時候,將寄存器上下文和棧都保存到其他地方,在切換回來的時候,恢復到先前保存的寄存器上下文和棧,因此:協程能保留上一次調用狀態,每次過程重入時,就相當於進入上一次調用的狀態。
協程的好處:
1.無需線程上下文切換的開銷(還是單線程)
2.無需原子操作(一個線程改一個變數,改一個變數的過程就可以稱為原子操作)的鎖定和同步的開銷
3.方便切換控制流,簡化編程模型
4.高併發+高擴展+低成本:一個cpu支持上萬的協程都沒有問題,適合用於高併發處理
缺點:
1.無法利用多核的資源,協程本身是個單線程,它不能同時將單個cpu的多核用上,協程需要和進程配合才能運用到多cpu上(協程是跑線上程上的)
2.進行阻塞操作時會阻塞掉整個程式:如io
使用yield實現協程的例子:
import time import queue def consumer(name): print("---->start eating baozi......") while True: #yield預設可以返回數據,走到yield整個程式返回,yield被喚醒的時候還可以接收數據 #進入死迴圈碰到yield暫停,被喚醒的時候才執行print new_baozi = yield print("[%s] is eating baozi %s" %(name,new_baozi)) #time.sleep(2) def producer(): #__next__()調用消費者的next,consumer直接調用的話,第一次不會執行,會變成一個生成器 #函數如果裡面有yield第一次加括弧調用,他是一個生成器,還沒有真正執行,__next__()才會執行 r = con.__next__() r = con2.__next__() n = 0 while n<5: n += 1 #send有兩個作用:喚醒生成器的同時,傳送一個值,傳的這個值就是yield接收到的值new_baozi con.send(n) con2.send(n)
#time.sleep(1) print("\033[32;1m[producer]\033[0m is making baozi %s" %n) if __name__ == '__main__': con = consumer("c1") con2 = consumer("c2") p = producer()
整個就是通過yield實現的一個簡單的協程,在程式運行過程中,直接完成運作,感覺像多併發的效果。
問題:他們好像能夠實現多併發的效果,是因為每一個生產者沒有任何的sleep,如果在producer中加一個time.sleep(1),那麼運行速度就變慢了。
遇到io操作就切換,io操作很耗時,協程之所以能夠處理大併發,就是把io操作去除,之後就變成這個程式只有cpu在切換
如何實現程式自動檢測io操作完成:
greenlet
from greenlet import greenlet def tesst1(): print(12) gr2.switch() print(34) gr2.switch() def tesst2(): print(56) gr1.switch() print(78) gr1 = greenlet(tesst1)#啟動一個協程 gr2 = greenlet(tesst2) gr1.switch()#手動切換
運行結果:
greenlet現在還是手動切換
gevent
Gevent是一個第三方庫,可以輕鬆通過gevent實現併發同步或非同步編程,在gevent中用到的主要模式是Greenlet,它是以c擴展模塊形式接入python的輕量級協程。Greenlet全部運行在主程式操作系統進程的內部,但他們被協作式的調度
import gevent def foo(): print("Running in foo") gevent.sleep(2) print("Explicit context switch to foo again") def bar(): print("Explicit context to bar") gevent.sleep(1) print("Implicit context to switch back to bar") gevent.joinall( [ gevent.spawn(foo), gevent.spawn(bar), ] )
運行結果:
運行整個也就運行了2秒,模擬io操作
下麵我們做一個簡單的小爬蟲:
from urllib.request import urlopen import gevent,time def f(url): print("GET: %s "%url) resp = urlopen(url) data = resp.read() print("%d bytes received from %s." %(len(data),url)) urls = [ 'https://www.python.org/', 'https://www.yahoo.com/', 'https://github.com/' ] time_start = time.time() for url in urls: f(url) print("同步cost",time.time()-time_start) async_time_start = time.time() gevent.joinall([ gevent.spawn(f,'https://www.python.org/'), gevent.spawn(f,'https://www.yahoo.com/'), gevent.spawn(f,'https://github.com/') ]) print("非同步cost",time.time()-time_start)
用了同步和非同步兩種方式來進行網頁的爬取,但是結果卻是同步的比非同步的用的時間還要短,我們之前用了gevent.sleep(1)來模擬io操作,順便就完成了執行任務。原因是:gevent調用urllib預設是堵塞的,gevent檢測不到urllib的io操作,所以不會進行切換,所以還是串列運行。
那怎麼才能夠讓gevent知道urllib是io操作呢,要用到一個模塊monkey
from urllib.request import urlopen import gevent,time from gevent import monkey monkey.patch_all()#把當前程式的所有io操作單獨的坐上標記 def f(url): print("GET: %s "%url) resp = urlopen(url) data = resp.read() print("%d bytes received from %s." %(len(data),url)) urls = [ 'https://www.python.org/', 'https://www.yahoo.com/', 'https://github.com/' ] time_start = time.time() for url in urls: f(url) print("同步cost",time.time()-time_start) async_time_start = time.time() gevent.joinall([ gevent.spawn(f,'https://www.python.org/'), gevent.spawn(f,'https://www.yahoo.com/'), gevent.spawn(f,'https://github.com/') ]) print("非同步cost",time.time()-async_time_start)
這樣就非同步爬取的速度瞬間快了很多
下麵我們在利用gevent寫一個socket伺服器端和客戶端:
伺服器端:
1 import sys,socket,time,gevent 2 3 from gevent import socket,monkey 4 monkey.patch_all() 5 6 def server(port): 7 s = socket.socket() 8 s.bind(('0.0.0.0',port)) 9 s.listen(500) 10 while True: 11 cli,addr = s.accept() 12 gevent.spawn(handle_request,cli) 13 def handle_request(conn): 14 try: 15 while True: 16 data = conn.recv(1024) 17 print("recv:",data) 18 conn.send(data) 19 if not data: 20 conn.shutdown(socket.SHUT_WR) 21 except Exception as e: 22 print(e) 23 finally: 24 conn.close() 25 26 if __name__ == '__main__': 27 server(8001)
和socketserver差不多,處理數據都是在handle_request函數中,然後在server中建立一個gevent
客戶端:
1 import socket 2 HOST = 'localhost' 3 PORT = 8001 4 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 5 s.connect((HOST,PORT)) 6 while True: 7 msg = bytes(input(">>>:"),encoding="utf-8") 8 s.sendall(msg) 9 data = s.recv(1024) 10 #repr格式化輸出 11 print("Recv:",repr(data)) 12 s.close()
運行結果:
由上面幾個運行結果可以知道:我們已經實行了併發運行,不需要使用什麼多線程。我們用協程,遇到io就阻塞
我們現在實現了切換,但是我們是什麼時候切換回來呢,我們怎麼知道什麼時候這個函數的東西執行完,切換到原函數呢
事件驅動與非同步IO
通常,我們寫伺服器處理模型的程式時,有一下幾個模型:
1.每收到一個請求,創建一個新的進程,來處理改請求:如socketserver
2.每收到一個請求,創建一個新的線程,來處理該請求:如socketserver
3.每收到一個請求,放入一個事件列表,讓主進程通過非阻塞I/O方式來處理請求,即事件驅動的模式,該模式是大多數網路伺服器採用的方式
在ui編程中,我們常常要對滑鼠點擊進行相應的反應,我們怎麼獲得滑鼠點擊呢?
目前大部分的UI編程都是事件驅動模型,如很多的ui平臺都會提供onclick()事件,這個事件就代表了滑鼠按下的事件。事件驅動模型大體思路:
1.有一個事件隊列
2.滑鼠按下時,往這個隊列中增加一個點擊事件
3.有個迴圈,不斷的從隊列中取出事件,根據不同的事件,調用不同 的函數,如:onclick(),onkeydown()等
4.事件(消息)一般都各自保存各自的處理函數指針,這樣,每個消息都有獨立的處理函數
那我們在回到上面的問題,io什麼時候切換回來。我們可以註冊一個回調函數,回調函數就是當你的程式一遇到io操作,就切換,然後等著io操作結束又切換回來。io操作是操作系統完成的。就是通過這個事件驅動。
寫的不是太好,希望大家多多包涵QAQ