並發編程之協程
主要知識點:
一、協程介紹
二、greenle模塊
三、gevent模塊
1?? 協程介紹
1、前言+回顧
1.1 並發的本質
基於單線程來實現並發,即只用一個主線程(很明顯可利用的cpu只有一個)情況下實現並發,為此我們需要先回顧下並發的本質:切換+保存狀態。
1.2 並發實現過程:
cpu正在運行一個任務,會在兩種情況下切走去執行其他的任務(切換由操作系統強制控制),一種情況是該任務發生了阻塞,
另外一種情況是該任務計算的時間過長或有一個優先級更高的程序替代了它。
1.3 圖示如下:
針對上圖的幾點理解:
第一點:第二種情況並不能提升效率,只是為了讓cpu能夠雨露均沾,實現看起來所有任務都被“同時”執行的效果,
如果多個任務都是純計算的,這種切換反而會降低效率。
我們之前學過的yield,就是一種在單線程下可以保存任務狀態的方法,我們一起回顧一下相關知識點:
#1、 yiled可以保存狀態,yield的狀態保存與操作系統的保存線程狀態很像,但是yield是代碼級別控制的,更輕量級
#2、send可以把一個函數的結果傳給另外一個函數,以此實現單線程內程序之間的切換
實例:
#!/usr/bin/env python3 #-*- coding:utf-8 -*-# write by congcong # 1、並發執行 import time def producer(): g = consumer() next(g) for i in range(1000000): g.send(i) def consumer(): while True: res = yield start_time = time.time() producer() stop_time = time.time() print(stop_time-start_time)
#0.12500381469726562 # 並發,純計算加來回切換(計算時長等於各個線程之和,總時長並不會減少),耗時反而長
第二點:第一種情況的切換。在任務一遇到io情況下,切到任務二去執行,這樣就可以利用任務一阻塞的時間
完成任務二的計算,效率的提升就在於此。
# 2、串行執行 import time def producer(): res = [] for i in range(1000000): res.append(i) return res def consumer(res): pass start_time = time.time() res = producer() consumer(res) stop_time = stop_time print(stop_time-start_time)
# 0.0 # 串行,僅計算,耗時反而短
註意:單線程下,我們不可避免程序中出現io操作,但如果我們能在自己的程序中(即用戶程序級別,而非操作系統級別)
控制單線程下的多個任務能在一個任務遇到io阻塞時就切換到另外一個任務去計算,這樣就保證了該線程能夠最大限度地
處於就緒態,即隨時都可以被cpu執行的狀態,相當於我們在用戶程序級別將自己的io操作最大限度地隱藏起來,從而可以
迷惑操作系統,讓其看到:該線程好像是一直在計算,io比較少,從而更多的將cpu的執行權限分配給我們的線程。
2、協程
2.1 協程的定義:
協程:是單線程下的並發,又稱微線程,纖程。英文名Coroutine。
簡單總結就是協程是一種用戶態的輕量級線程,即協程是由用戶程序自己控制調度的。
2.2 協程的本質:
在單線程下,由用戶自己控制一個任務遇到io阻塞了就切換另外一個任務去執行,以此來提升效率。
為了實現它,我們需要找尋一種可以同時滿足以下條件的解決方案:
#1. 可以控制多個任務之間的切換,切換之前將任務的狀態保存下來,以便重新運行時,可以基於暫停的位置繼續執行。
#2. 作為1的補充:可以檢測io操作,在遇到io操作的情況下才發生切換
協程和線程的區別:線程由操作系統控制切換,而協程是用戶在單線程內控制的切換。
需要強調的是:
#1. python的線程屬於內核級別的,即由操作系統控制調度(如單線程遇到io或執行時間過長就會被迫交出cpu執行權限,切換其他線程運行)
#2. 單線程內開啟協程,一旦遇到io,就會從應用程序級別(而非操作系統)控制切換,以此來提升效率(!!!非io操作的切換與效率無關)
··· 2.3 協程的優缺點
優點:
1. 協程的切換開銷更小,屬於程序級別的切換,操作系統完全感知不到,因而更加輕量級
2. 單線程內就可以實現並發的效果,最大限度地利用cpu
缺點:
1. 協程的本質是單線程下,無法利用多核,可以是一個程序開啟多個進程,每個進程內開啟多個線程,每個線程內開啟協程
2. 協程指的是單個線程,因而一旦協程出現阻塞,將會阻塞整個線程
2.4 總結協程的特點
1、必須只有一個單線程裏實現並發;
2、修改共享數據不需加鎖;
3、用戶程序裏自己保存多個控制流的上下文棧;
4、附加:一個協程遇到IO操作自動切換到其它協程(如何實現檢測IO,yield、greenlet都無法實現,就用到了gevent模塊(select機制))
2?? greenlet模塊
1、為什麽需要greenlet模塊
如果我們在單個線程內有20個任務,要想實現在多個任務之間切換,使用yield生成器的方式過於麻煩(需要先得到初始化一次的生成器,
然後再調用send,非常麻煩),而使用greenlet模塊可以非常簡單地實現這20個任務直接的切換。
2、實例:
註意:需要先安裝greenlet模塊(進入管理員cmd窗口中輸入:pip install greenlet)
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# write by congcong
from greenlet import greenlet
import time
def eat(name):
print(‘%s eats 1‘%name)
#time.sleep(6) # 會一直卡在此處,表明無法檢測IO阻塞
g2.switch(‘cc‘) # 僅第一次需要傳參,切換到play函數
print(‘%s eats 2‘%name)
g2.switch()
def play(name):
print(‘%s plays 1‘%name)
g1.switch() # 不需要傳參,切換到eat函數
print(‘%s plays 2‘%name)
g1 = greenlet(eat)
g2 = greenlet(play)
g1.switch(‘cc‘) # 僅第一次需要傳參
‘‘‘
cc eats 1
cc plays 1
cc eats 2
cc plays 2
‘‘‘
3、不足:
greenlet只是提供了一種比generator更加便捷的切換方式,當切到一個任務執行時如果遇到io,
那就原地阻塞,仍然是沒有解決遇到IO自動切換來提升效率的問題。
3?? gevent模塊
1、Gevent 是一個第三方庫,可以輕松通過gevent實現並發同步或異步編程,
在gevent中用到的主要模式是Greenlet, 它是以C擴展模塊形式接入Python的輕量級協程。
Greenlet全部運行在主程序操作系統進程的內部,但它們被協作式地調度。
2、用法:
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的返回值
3、實例:
註意:首先安裝genent模塊(pip install gevent);要用gevent,需要將from gevent import monkey;monkey.patch_all()放到文件的開頭.
gevent是不能直接識別的需要用下面一行代碼,打補丁,就可以識別了
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# write by congcong
from gevent import monkey;monkey.patch_all() # 必須放到被打補丁者的前面,如time,socket模塊之前
import gevent
import time
def eat(name):
print(‘%s eats 1‘%name)
time.sleep(3) # 模擬IO阻塞,使CPU切換這個線程的其他任務執行
print(‘%s eats 2‘%name)
def play(name):
print(‘%s play 1‘%name)
time.sleep(4)
print(‘%s play 2‘%name)
g1 = gevent.spawn(eat,‘cc‘)
g2 = gevent.spawn(play,‘cc‘)
# time.sleep(5) 不科學,當並發的時間未知時不可用
# g1.join()
# g2.join()
gevent.joinall([g1,g2]) # 與前兩個表達式的效果相同,等待所有進程結束
‘‘‘運行結果:
cc eats 1
cc play 1
cc eats 2
cc play 2
‘‘‘
4?? 練習:通過gevent實現單線程下的socket並發。
註意:from gevent import monkey;monkey.patch_all()一定要放到導入socket模塊之前,否則gevent無法識別socket的阻塞
參考答案:
服務端:
from gevent import monkey;monkey.patch_all()
from socket import *
import gevent
#如果不想用money.patch_all()打補丁,可以用gevent自帶的socket
# from gevent import socket
# s=socket.socket()
def server(server_ip,port):
s=socket(AF_INET,SOCK_STREAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind((server_ip,port))
s.listen(5)
while True:
conn,addr=s.accept()
gevent.spawn(talk,conn,addr)
def talk(conn,addr):
try:
while True:
res=conn.recv(1024)
print(‘client %s:%s msg: %s‘ %(addr[0],addr[1],res))
conn.send(res.upper())
except Exception as e:
print(e)
finally:
conn.close()
if __name__ == ‘__main__‘:
server(‘127.0.0.1‘,8080)
View Code
客戶端(實現多線程並發):
from threading import Thread
from socket import *
import threading
def client(server_ip,port):
c=socket(AF_INET,SOCK_STREAM) #套接字對象一定要加到函數內,即局部名稱空間內,放在函數外則被所有線程共享,則大家公用一個套接字對象,那麽客戶端端口永遠一樣了
c.connect((server_ip,port))
count=0
while True:
c.send((‘%s say hello %s‘ %(threading.current_thread().getName(),count)).encode(‘utf-8‘))
msg=c.recv(1024)
print(msg.decode(‘utf-8‘))
count+=1
if __name__ == ‘__main__‘:
for i in range(500):
t=Thread(target=client,args=(‘127.0.0.1‘,8080))
t.start()
View Code
並發編程之協程