I/O操做,不佔用CPU。它內部有一個專門的處理I/O模塊。
print和寫log 屬於I/O操做,它不佔用CPUpython
線程編程
GIL鎖保證一個進程中的多個線程在同一時刻只有一個能夠被CPU執行數組
實例化一個LOCK( ),它就是一個互斥鎖緩存
lock 和 rlock 安全
互斥鎖LOCK多線程
死鎖併發
rlock 遞歸鎖app
遞歸鎖不會發送死鎖現象異步
2個進程中的線程,不會受到GIL影響socket
GIL 是針對一個進程中的多個線程,同一時間,只能有一個線程訪問CPU
針對GIL的CPU利用率問題
起多個進程,就能夠解決CPU利用率問題。
科學家吃麪的例子,它不能用一把鎖,必須2個鎖。
def eat1(noodle_lock,fork_lock,name): noodle_lock.acquire() print('%s搶到了面'%name) fork_lock.acquire() print('%s搶到了叉子'%name) print('%s正在吃麪'%name) fork_lock.release() print('%s歸還了叉子' % name) noodle_lock.release() print('%s歸還了面' % name)
假設有三我的,
A要面和叉子
B只要面
C只要叉子
若是隻有一個鎖,那麼就沒法處理這3我的的需求,會發生數據不安全的狀況。
semaphore 在開始固定一個線程的流量
condition 經過信號控制線程的流量
event 經過一個信號控制全部線程
timer 定時器
隊列 線程數據安全
線程池
可以在多線程的基礎上進一步節省內存和時間開銷
semaphore 在一開始固定一個線程的流量
condition 經過一個信號控制線程的流量
event 經過一個信號控制全部線程
timer 定時器
隊列 線程數據安全
線程池
可以在多線程的基礎上進一步節省內存和時間開銷
以前咱們學習了線程、進程的概念,瞭解了在操做系統中進程是資源分配的最小單位,線程是CPU調度的最小單位。按道理來講咱們已經算是把cpu的利用率提升不少了。可是咱們知道不管是建立多進程仍是建立多線程來解決問題,都要消耗必定的時間來建立進程、建立線程、以及管理他們之間的切換。
隨着咱們對於效率的追求不斷提升,基於單線程來實現併發又成爲一個新的課題,即只用一個主線程(很明顯可利用的cpu只有一個)狀況下實現併發。這樣就能夠節省建立線進程所消耗的時間。
爲此咱們須要先回顧下併發的本質:切換+保存狀態
cpu正在運行一個任務,會在兩種狀況下切走去執行其餘的任務(切換由操做系統強制控制),一種狀況是該任務發生了阻塞,另一種狀況是該任務計算的時間過長
ps:在介紹進程理論時,說起進程的三種執行狀態,而線程纔是執行單位,因此也能夠將上圖理解爲線程的三種狀態
一:其中第二種狀況並不能提高效率,只是爲了讓cpu可以雨露均沾,實現看起來全部任務都被「同時」執行的效果,若是多個任務都是純計算的,這種切換反而會下降效率。
爲此咱們能夠基於yield來驗證。yield自己就是一種在單線程下能夠保存任務運行狀態的方法,咱們來簡單複習一下:
#1 yiled能夠保存狀態,yield的狀態保存與操做系統的保存線程狀態很像,可是yield是代碼級別控制的,更輕量級 #2 send能夠把一個函數的結果傳給另一個函數,以此實現單線程內程序之間的切換
import time def consumer(res): '''任務1:接收數據,處理數據''' pass def producer(): '''任務2:生產數據''' res=[] for i in range(10000000): res.append(i) return res start=time.time() #串行執行 res=producer() consumer(res) #寫成consumer(producer())會下降執行效率 stop=time.time() print(stop-start) #1.1190457344055176 #基於yield併發執行 import time def consumer(): while True: x = yield def producer(): g = consumer() next(g) for i in range(1000000): g.send(i) start=time.time() producer() stop = time.time() print(stop-start) #0.10477948188781738
二: 第一種狀況的切換。在任務一遇到IO狀況下,切換到任務二去執行,這樣就能夠利用任務----阻塞的時間完成任務二的計算,效率的提高就在於此。
import time def consumer(): '''任務1:接收數據,處理數據''' while True: x=yield def producer(): '''任務2:生產數據''' g=consumer() next(g) for i in range(10000000): g.send(i) time.sleep(2) start=time.time() producer() #併發執行,可是任務producer遇到io就會阻塞住,並不會切到該線程內的其餘任務去執行 stop=time.time() print(stop-start)
對於單線程下,咱們不可避免程序中出現IO操做,但若是咱們能在本身的程序中(即用戶程序級別,而非操做系統級別)控制單線程下的多個任務能在一個任務遇到IO阻塞時就切換到另一個任務去計算,這樣就保證了改線程可以最大限度的處於就緒狀態。即隨時可均可以被CPU執行的狀態,至關於咱們在用戶程序級別將本身的IO操做最大限度的隱藏起來,從而能夠迷惑操做系統,讓其看到: 該線程好像一直是計算,io 比較少,從而更多的將CPU執行權限分配給咱們的線程。
協成的本質就是在單線程下,由用戶本身控制一個任務遇到io阻塞了就切換另一個任務去執行,以此來提高效率。爲了實現它,咱們須要尋找一種能夠同時知足如下條件的解決方案:
1.能夠控制多個任務之間的切換,切換以前將任務的狀態保存下來,以便從新運行時,能夠基於暫停的位置繼續執行。
2.做爲1的補充:能夠監測IO操做,在遇到io遇到的狀況下才發生切換。
二,協程介紹
協程:是單線程下的併發,又稱微線程,纖程。一句話說明什麼是線程: 協程是一種用戶態的輕量級線程,即協程是由用戶程序本身控制調度的。
須要強調的是:
1.python的線程屬於內核級別的,即由操做系統控制調度 (如單線程遇到IO或執行時間就會被迫交出CPI執行權限,切換其餘線程運行)
2.單線程內核開啓協程,一旦遇到IO,就會從應用程序級別(而非操做系統)控制切換,以此來提高效率(!!!非io操做的切換與效率無關)
對比操做系統控制線程的切換,用戶在單線程內控制協程的切換
優勢以下:
#1. 協程的切換開銷更小,屬於程序級別的切換,操做系統徹底感知不到,於是更加輕量級 #2. 單線程內就能夠實現併發的效果,最大限度地利用cpu
缺點以下:
#1. 協程的本質是單線程下,沒法利用多核,能夠是一個程序開啓多個進程,每一個進程內開啓多個線程,每一個線程內開啓協程 #2. 協程指的是單個線程,於是一旦協程出現阻塞,將會阻塞整個線程
總結協程特色:
安裝 :pip3 install greenlet
from greenlet import greenlet def eat(name): print('%s eat 1'%name) g2.switch('egon') print('%s eat 2'%name) g2.switch() def play(name): print('%s play 1'%name) g1.switch() print('%s play 2'%name) g1=greenlet(eat) g2=greenlet(play) g1.switch('egon')
單純的切換(在沒有IO的狀況下或者沒有重複內存空間的操做),反而會下降程序的執行速度
#順序執行 import time def f1(): res=1 for i in range(100000000): res+=i def f2(): res=1 for i in range(100000000): res*=i start=time.time() f1() f2() stop=time.time() print('run time is %s' %(stop-start)) #10.985628366470337 #切換 from greenlet import greenlet import time def f1(): res=1 for i in range(100000000): res+=i g2.switch() def f2(): res=1 for i in range(100000000): res*=i g1.switch() start=time.time() g1=greenlet(f1) g2=greenlet(f2) g1.switch() stop=time.time() print('run time is %s' %(stop-start)) # 52.763017892837524 效率對比
greenlet只是提供了一種比generator更加便捷的切換方式,當切到一個任務執行時若是遇到io,那就原地阻塞,仍然是沒有解決遇到IO自動切換來提高效率的問題。
單線程裏的這20個任務的代碼一般會既有計算操做又有阻塞操做,咱們徹底能夠在執行任務1時遇到阻塞,就利用阻塞的時間去執行任務2。。。。如此,才能提升效率,這就用到了Gevent模塊。
安裝:pip3 install gevent
Gevent 是一個第三方庫,能夠輕鬆經過gevent實現併發同步或異步編程,在gevent中用到的主要模式是Greenlet, 它是以C擴展模塊形式接入Python的輕量級協程。 Greenlet所有運行在主程序操做系統進程的內部,但它們被協做式地調度。
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的返回值
from gevent import monkey;monkey.patch_all() import gevent import time def eat(): print('eat food 1') time.sleep(2) print('eat food 2') def play(): print('play 1') time.sleep(1) print('play 2') g1=gevent.spawn(eat) g2=gevent.spawn(play) gevent.joinall([g1,g2]) 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().getName()) print('eat food 1') time.sleep(2) print('eat food 2') def play(): print(threading.current_thread().getName()) print('play 1') time.sleep(1) print('play 2') g1 = gevent.spawn(eat) g2 = gevent.spawn(play) gevent.joinall([g1,g2]) print('主')
from gevent import spawn,joinall,monkey;monkey.patch_all() import time def task(pid): time.sleep(0.5) print('Task %s done'% pid) def synchronous(): for i in range(10): task(i) def asynchronous(): g_1 = [spawn(task,i)for i in range(10)] joinall(g_1) print('DONE') if __name__ == '__main__': print('Synchronous:') synchronous() print('Asynchronous:') asynchronous() # 上面程序的重要部分是將task函數封裝到Greenlet內部線程的gevent.spawn。 # 初始化的greenlet列表存放在數組threads中,此數組被傳給gevent.joinall 函數,
對比使用普通函數和使用協程,誰更快一點
因爲操做系統,訪問一次網頁後,會有緩存。
因此測試時,先訪問一遍網頁。再分別測試協程和普通函數。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
from
gevent
import
monkey;monkey.patch_all()
from
urllib.request
import
urlopen
import
gevent
import
time
def
get_page(url):
res
=
urlopen(url)
#print(len(res.read()))
url_lst
=
[
'http://www.baidu.com'
,
'http://www.sogou.com'
,
'http://www.sohu.com'
,
'http://www.qq.com'
,
'http://www.cnblogs.com'
,
]
start
=
time.time()
gevent.joinall([gevent.spawn(get_page,url)
for
url
in
url_lst])
print
(
'先執行一次'
,time.time()
-
start)
start
=
time.time()
gevent.joinall([gevent.spawn(get_page,url)
for
url
in
url_lst])
print
(
'協程'
,time.time()
-
start)
start
=
time.time()
for
url
in
url_lst:get_page(url)
print
(
'普通'
,time.time()
-
start)
|
執行輸出:
先執行一次 0.6465449333190918
協程 0.34525322914123535
普通 0.570899486541748
結論
之後用爬蟲,可使用協程,它的速度更快。
Gevent 之應用舉例二
經過gevent實現單線程下的socket併發
注意:from gevent import monkey;monkey.patch_all( ) 必定要放到導入socket模塊以前,不然gevent沒法識別socket的阻塞