本文目錄一覽:
在python中線程和協程的區別是什麼
在python中線程和協程的區別:1、一個線程可以擁有多個協程,這樣在python中就能使用多核CPU;2、線程是同步機制,而協程是非同步;3、 協程能保留上一次調用時的狀態,每次過程重入時,就相當於進入上一次調用的狀態。
一、首先我們來了解一下線程和協程的概念
1、線程
線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。線程間通信主要通過共享內存,上下文切換很快,資源開銷較少,但相比進程不夠穩定容易丟失數據。
2、協程
協程是一種用戶態的輕量級線程,協程的調度完全由用戶控制。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操作棧則基本沒有內核切換的開銷,可以不加鎖的訪問全局變數,所以上下文的切換非常快。
二、協程與線程的比較
1) 一個線程可以擁有多個協程,一個進程也可以單獨擁有多個協程,這樣python中則能使用多核CPU。
2) 線程進程都是同步機制,而協程則是非同步。
3) 協程能保留上一次調用時的狀態,每次過程重入時,就相當於進入上一次調用的狀態。
三、線程、協程在python中的使用
1、多線程一般是使用threading庫,完成一些IO密集型並發操作。多線程的優勢是切換快,資源消耗低,但一個線程掛掉則會影響到所有線程,所以不夠穩定。現實中使用線程池的場景會比較多,具體可參考《python線程池實現》。
2、協程一般是使用gevent庫,當然這個庫用起來比較麻煩,所以使用的並不是很多。相反,協程在tornado的運用就多得多了,使用協程讓tornado做到單線程非同步,據說還能解決C10K的問題。所以協程使用的地方最多的是在web應用上。
總結一下:
IO密集型一般使用多線程或者多進程,CPU密集型一般使用多進程,強調非阻塞非同步並發的一般都是使用協程,當然有時候也是需要多進程線程池結合的,或者是其他組合方式。
推薦課程:Python高級進階視頻教程
python 中的協程是怎麼實現多任務的?
協程也稱為微線程,是在一個線程中,通過不斷的切換任務函數實現了多任務的效果。
協程在python實現的原理主要是通過yield這個關鍵字實現
但是真正在開發時,可以不需要自己實現,可以通過很多成熟的第三方模塊來實現協程,比如greenlet,gevent等模塊。多線程的課程我記得是在黑馬程序員裡面找的,一套,還有資料。
Python協程之asyncio
asyncio 是 Python 中的非同步IO庫,用來編寫並發協程,適用於IO阻塞且需要大量並發的場景,例如爬蟲、文件讀寫。
asyncio 在 Python3.4 被引入,經過幾個版本的迭代,特性、語法糖均有了不同程度的改進,這也使得不同版本的 Python 在 asyncio 的用法上各不相同,顯得有些雜亂,以前使用的時候也是本著能用就行的原則,在寫法上走了一些彎路,現在對 Python3.7+ 和 Python3.6 中 asyncio 的用法做一個梳理,以便以後能更好的使用。
協程,又稱微線程,它不被操作系統內核所管理,而完全是由程序控制,協程切換花銷小,因而有更高的性能。
協程可以比作子程序,不同的是,執行過程中協程可以掛起當前狀態,轉而執行其他協程,在適當的時候返回來接著執行,協程間的切換不需要涉及任何系統調用或任何阻塞調用,完全由協程調度器進行調度。
Python 中以 asyncio 為依賴,使用 async/await 語法進行協程的創建和使用,如下 async 語法創建一個協程函數:
在協程中除了普通函數的功能外最主要的作用就是:使用 await 語法等待另一個協程結束,這將掛起當前協程,直到另一個協程產生結果再繼續執行:
asyncio.sleep() 是 asyncio 包內置的協程函數,這裡模擬耗時的IO操作,上面這個協程執行到這一句會掛起當前協程而去執行其他協程,直到sleep結束,當有多個協程任務時,這種切換會讓它們的IO操作並行處理。
注意,執行一個協程函數並不會真正的運行它,而是會返回一個協程對象,要使協程真正的運行,需要將它們加入到事件循環中運行,官方建議 asyncio 程序應當有一個主入口協程,用來管理所有其他的協程任務:
在 Python3.7+ 中,運行這個 asyncio 程序只需要一句: asyncio.run(main()) ,而在 Python3.6 中,需要手動獲取事件循環並加入協程任務:
事件循環就是一個循環隊列,對其中的協程進行調度執行,當把一個協程加入循環,這個協程創建的其他協程都會自動加入到當前事件循環中。
其實協程對象也不是直接運行,而是被封裝成一個個待執行的 Task ,大多數情況下 asyncio 會幫我們進行封裝,我們也可以提前自行封裝 Task 來獲得對協程更多的控制權,注意,封裝 Task 需要 當前線程有正在運行的事件循環 ,否則將引 RuntimeError,這也就是官方建議使用主入口協程的原因,如果在主入口協程之外創建任務就需要先手動獲取事件循環然後使用底層方法 loop.create_task() ,而在主入口協程之內是一定有正在運行的循環的。任務創建後便有了狀態,可以查看運行情況,查看結果,取消任務等:
asyncio.create_task() 是 Python3.7 加入的高層級API,在 Python3.6,需要使用低層級API asyncio.ensure_future() 來創建 Future,Future 也是一個管理協程運行狀態的對象,與 Task 沒有本質上的區別。
通常,一個含有一系列並發協程的程序寫法如下(Python3.7+):
並發運行多個協程任務的關鍵就是 asyncio.gather(*tasks) ,它接受多個協程任務並將它們加入到事件循環,所有任務都運行完成後會返回結果列表,這裡我們也沒有手動封裝 Task,因為 gather 函數會自動封裝。
並發運行還有另一個方法 asyncio.wait(tasks) ,它們的區別是:
python協程gevent怎麼用
在學習gevent之前,你肯定要知道你學的這個東西是什麼。
官方描述gevent
gevent is a coroutine-based Python networking library that uses greenlet to provide a high-level synchronous API on top of the libev event loop.
翻譯:gevent是一個基於協程的Python網路庫。我們先理解這句,也是這次學習的重點——協程。
wiki描述協程
與子常式一樣,協程也是一種程序組件。相對子常式而言,協程更為一般和靈活,但在實踐中使用沒有子常式那樣廣泛。子常式的起始處是惟一的入口點,一旦退出即完成了子常式的執行,子常式的一個實例只會返回一次;協程可以通過yield來調用其它協程。通過yield方式轉移執行權的協程之間不是調用者與被調用者的關係,而是彼此對稱、平等的。協程允許多個入口點,可以在指定位置掛起和恢復執行。
沒看懂?沒關係,我也沒看懂,不過算是有點線索:子常式。
子常式
過程有兩種,一種叫子常式(Subroutine),通常叫Sub;另一種叫函數(Function)。底層實現機制是一樣的,區別在於,Sub只執行操作,沒有返回值;Function不但執行操作,並且有返回值。用過VB的應該會比較清楚這點。(原諒我用了百度百科)說到底子常式就是過程,我們一般叫它函數。
說到函數,我就想吐槽了,不明白為什麼要叫函數。很多時候我們寫一個函數是為了封裝、模塊化某個功能,它是一個功能、或者說是一個過程。因為它包含的是類似於流程圖那樣的具體邏輯,先怎樣做,然後怎樣做;如果遇到A情況則怎樣,如果遇到B情況又怎樣。個人覺得還是叫過程比較好,叫做函數就讓人很糾結了,難道因為回歸到底層還是計算問題,出於數學的角度把它稱為函數?這個略坑啊!為了符合大家的口味,我還是稱之為函數好了(其實我也習慣叫函數了%_
講到函數,我們就往底層深入一點,看看下面的代碼:
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def a():
print “a start”
b()
print “a end”
def b():
print “b start”
c()
print “b end”
def c():
print “c start”
print “c end”
if __name__ == “__main__”:
a()
a start
b start
c start
c end
b end
a end
對於這樣的結果大家肯定不會意外的。每當函數被調用,就會在棧中開闢一個棧空間,調用結束後再回收該空間。
假設一個這樣的場景:有個講台,每個人都可以上去發表言論,但是每次講台只能站一個人。現在a在上面演講,當他說到「大家好!」的時候,b有個緊急通知要告訴大家,所以a就先下來讓b講完通知,然後a再上講台繼續演講。如果用函數的思想模擬這個問題,堆棧示意圖是這樣的:
大家會不會發現問題,就是b通知完a繼續演講都要重新開始。因為函數在重新調用的時候,它的局部變數是會被重置的,對於之前他說的那句「大家好」,他是不會記得的(可能a的記性不好)。那有沒有什麼辦法可以不讓他重複,而是在打斷之後繼續呢?很簡單,在他走下講台之前記住當前說過的話。表現在函數中就是在退出之前,保存該函數的局部變數,方便在重新進入該函數的時候,能夠從之前的局部變數開始繼續執行。
升級版
如果你有一段代碼生產數據,另外一段代碼消費數據,哪個應該是調用者,哪個應該是被調用者?
例如:生產者 —— 消費者問題,先拋開進程、線程等實現方法。假設有兩個函數producer和consumer,當緩衝區滿了,producer調用consumer,當緩衝區空了,consumer調用producer,但是這樣的函數互相調用會出什麼問題?
Python
1
2
3
4
5
6
7
8
def producer():
print “生產一個”
consumer()
def consumer():
print “消費一個”
producer()
producer生產一個,緩衝區滿了,consumer消費一個,緩衝區空了,producer生產一個,如此循環。會看到下面這樣的圖:
看起來好像不錯,感覺兩個函數協調運行的很好,很好的解決了生產者——消費者問題。如果真有這麼好也就不會有協程的存在了,仔細分析會有兩個問題:
無限次數的函數嵌套調用,而沒有函數返回,會有什麼樣的後果?
兩個函數貌似協調有序的工作,你來我往,但每次執行的都是同一個函數實例嗎?
首先,上面的偽代碼示例是一個無限的函數嵌套調用,沒有函數返回來釋放棧,棧的空間不斷的在增長,直到溢出,程序崩潰。然後,看起來兩個函數協調有序,事實上操作的都不是同一個實例對象,不知道下面的圖能否看懂。
那什麼東西有這樣的能力呢?我們很快就可以想到進程、線程,但是你真的想使用進程、線程如此重量級的東西在這麼簡單的程序上嗎?野蠻的搶佔式機制和笨重的上下文切換!
還有一種程序組件,那就是協程。它能保留上一次調用時的狀態,每次重新進入該過程的時候,就相當於回到上一次離開時所處邏輯流的位置。協程的起始處是第一個入口點,在協程里,返回點之後是接下來的入口點。協程的生命期完全由他們的使用的需要決定。每個協程在用yield命令向另一個協程交出控制時都儘可能做了更多的工作,放棄控制使得另一個協程從這個協程停止的地方開始,接下來的每次協程被調用時,都是從協程返回(或yield)的位置接著執行。
從上面這些你就可以知道其實協程是模擬了多線程(或多進程)的操作,多線程在切換的時候都會有一個上下文切換,在退出的時候將現場保存起來,等到下一次進入的時候從保存的現場開始,繼續執行。
看下協程是怎樣實現的:
Python
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
28
29
30
31
import random
from time import sleep
from greenlet import greenlet
from Queue import Queue
queue = Queue(1)
@greenlet
def producer():
chars = [‘a’, ‘b’, ‘c’, ‘d’, ‘e’]
global queue
while True:
char = random.choice(chars)
queue.put(char)
print “Produced: “, char
sleep(1)
consumer.switch()
@greenlet
def consumer():
global queue
while True:
char = queue.get()
print “Consumed: “, char
sleep(1)
producer.switch()
if __name__ == “__main__”:
producer.run()
consumer.run()
應用場景
我們一直都在大談協程是什麼樣一個東西,卻從沒有提起協程用來幹嘛,這個其實大家分析一下就能夠知道。從上面的生產者——消費者問題應該能看出,它分別有兩個任務,假設交給兩個人去執行,但每次只能允許一個人行動。當緩衝區滿的時候,生產者是出於等待狀態的,這個時候可以將執行任務的權利轉交給消費者,當緩衝區空得時候,消費者是出於等待狀態的,這個時候可以將執行任務的權利轉交給生產者,是不是很容易聯想到多任務切換?然後想到線程?最後想到高並發?
但同學們又會問,既然有了線程為什麼還要協程呢?因為線程是系統級別的,在做切換的時候消耗是特別大的,具體為什麼這麼大等我研究好了再告訴你;同時線程的切換是由CPU決定的,可能你剛好執行到一個地方的時候就要被迫終止,這個時候你需要用各種措施來保證你的數據不出錯,所以線程對於數據安全的操作是比較複雜的。而協程是用戶級別的切換,且切換是由自己控制,不受外力終止。
總結
協程其實模擬了人類活動的一種過程。例如:你準備先寫文檔,然後修復bug。這時候接到電話說這個bug很嚴重,必須立即修復(可以看作CPU通知)。於是你暫停寫文檔,開始去填坑,終於你把坑填完了,你回來寫文檔,這個時候你肯定是接著之前寫的文檔繼續,難道你要把之前寫的給刪了,重新寫?這就是協程。那如果是子常式呢?那你就必須重新寫了,因為退出之後,棧幀就會被彈出銷毀,再次調用就是開闢新的棧空間了。
總結:協程就是用戶態下的線程,是人們在有了進程、線程之後仍覺得效率不夠,而追求的又一種高並發解決方案。為什麼說是用戶態,是因為操作系統並不知道它的存在,它是由程序員自己控制、互相協作的讓出控制權而不是像進程、線程那樣由操作系統調度決定是否讓出控制權。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/231579.html