Python的線程、進程與協程分別實現多任務詳細過程以及區別對比 O(≧▽≦)O Python小知識

併發與並行

  1. 併發:同時發生,輪流處理
    假設,我們電腦只有一個CPU,同時只能處理一個線程,我們開機後運行的QQ,瀏覽器,還有衆多系統服務,這些軟件和服務只能輪流去調用則這個CPU,這種情況就屬於併發(但因爲每個軟件只需要幾毫秒甚至更少的時間就能執行完成,所以這時候我們並不會感覺到有明顯卡頓等狀況。)
    在這裏插入圖片描述
  2. 並行:同時發生,同時處理
    假設,當電腦中有多個CPU時,QQ和瀏覽器在不同的CPU中運行,這種情況就屬於並行。(但電腦中因爲會同時運行大量服務和軟件,所以併發的情況比並行要更加普遍。)
    在這裏插入圖片描述

線程與進程

  • 線程: 任務調度和執行的基本單位

    • 假設打開QQ,同一個QQ中,同時打開衆多聊天窗口,而這每個窗口都是一個線程。而QQ是一個進程。線程要依附於進程存在,同進程內切換線程消耗資源少,切同一進程內的所以線程都使用同一份共享資源。
      -
  • 進程: 操作系統資源分配的基本單位

    • 假設打開QQ音樂與網易雲音樂,這兩個程序就是啓動後分別稱爲兩個進程,而在這兩個進程真我們同時進行音樂播放和下載,這兩個功能分別爲這兩個進程中的兩個線程。我們可以看出進程中進行通信成本要高於線程,且進程切換的成本較高,但進程的優勢是能更好的利用多處理器。
      在這裏插入圖片描述

同步、異步與阻塞、非阻塞

  • 同步: 當存在IO操作時,必須等到IO操作結束才能在進行下一步操作(如:當程序碰到用戶輸入時必須等待用戶輸入)
  • 異步: 當存在IO操作時,程序不必等到IO操作也能繼續運行。(如當程序碰到用戶輸入時,不必等待用戶輸入)
  • 阻塞: 當程序在運行時被卡住.導致程序不再繼續向下運行,需要等待,即爲阻塞
  • 非阻塞: 程序運行沒有卡住,持續運行,無需等待,即爲非阻塞

Python的多線程與多進程

因爲Python的GIL(globalinterpreterlock,全局解釋鎖)的存在,在一個程序內只能同時運行一個進程,所以我們可以將Python的多線程理解爲一個人同時幹多個事與多個人同時幹多個事情的區別。(GIL是CPython的一個特性,雖然限制了Python的性能,Python社區也嘗試過去掉GIL,但GIL的主要作用是爲了一個資源同時只會被一個線程佔用,這樣不僅讓共享資源變得更加可信,同時也讓死鎖不會再Python的多線程中發生)
Python使用多線程時,內部資源是共享的;而多進程之間資源是不共享的,不過我們可以通過進程之間的通信來解決這個問題。

Python多線程

同一個程序如何在同時能運行多個功能模塊?比如在通信的時候,我們如何做到在一遍監視通信端口時候收到信息,一邊去發送信息?這時候就需要用到多線程,Python中的內置庫中就包含多線程庫threadingthreading官方庫文檔

創建函數多線程

首先我們要用到threading庫中的.Thread()方法,並且方法中需要用到關鍵字參數傳入需要創建的函數目標參數target=
.Thread()方法中的參數順序分別爲group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None

Thread()方法的參數 作用
group 預留參數,當前無作用。
target 指定需要執行的函數。(官方點的解釋是用run()方法調用的對象)
name 線程名稱。默認名稱爲(Thread-N,N是創建的第幾個線程)
args與kwargs 作用相似,都是傳入指定函數所帶的參數 ,不同的是args是位置傳參,kwargs是關鍵字傳參。傳入的參數會成爲全局共享參數。
daemon 守護進程,默認關閉(False),包括主線程的守護進程默認也爲關閉的非守護線程。

衆所周知,程序員時間緊缺,所以我們就來用Thread來解決一遍喫飯一遍敲碼的需求吧。

# 多線程
import threading
import time


# 創建多線程的函數
def eat():
    for i in range(3):
        print('我在喫飯')
        time.sleep(1)


def code():
    for i in range(3):
        print('我在敲碼')
        time.sleep(1)


if __name__ == '__main__':
    # 是Thread方法創建新的線程,將需要創建新線程的函數用目標(target)參數傳入
    demo1 = threading.Thread(target=eat)
    demo2 = threading.Thread(target=code)
    demo1.start()
    demo2.start()

在使用Thread()方法創建好線程後,我們需要使用srart()方法開始線程活動。
線程被srart()啓動後,Python就會在一個獨立的新線程中調用run()方法(我們傳入的函數就在此方法中),線程啓動後被判定爲’存活的’線程,當線程正常或因爲異常退出時,線程就不在是’存活的’。

查看線程

  1. 使用.is_alive()方法可以查看指定線程當前是否存活。
  2. 使用.enumerate()方法可以查看當前一共有哪些存活線程。
    可以使用.Tread()方法的name參數給線程指定名稱
    # 查看線程1是否存活
    print(demo1.is_alive())
    # 查看啓動的線程數
    print(threading.enumerate())

在這裏插入圖片描述

用繼承方式創建多線程類

上述我們多次提到.run()方法,在繼承中我們就能更完整的看到他的實際意義。

class ThreadDemo(threading.Thread):

    def __init__(self, name='自定義線程'):
        super().__init__()
        # 自動定義一個線程名字
        self.name = name

    def run(self):
        for i in range(5):
            print('我在run方法中', i)
            time.sleep(1)


if __name__ == '__main__':
    # 繼承多線程類
    demo = ThreadDemo('測試')
    demo.start()
    print(threading.enumerate())

在這裏插入圖片描述
可以看到在繼承threading.Thread後,我們創建的類實例化後調用.staet()方法,運行的卻是.run()方法,如果你想直接調用.run()方法雖然同樣不會報錯,但是並不會創建新線程,還是會在主線程中運行。

多線程共享全局變量(線程間通信)

如果我們在正常單線程中只需要形參與實參就能像函數內傳入參數,進行通信,而多線程中,我們如果想要進行通信就需要用到.Thread()方法中的args與kwars屬性,其中args我們可以看做爲位置傳參,而kwars則爲關鍵字傳參使用。
args屬性接收元組,kwars屬性接收字典

import threading
import time
x = 0
lis = [1, 2]


# 創建多線程的函數
def eat(a, items):
    for i in range(3):
        a += 1
        items.append(a)
        print('我在喫飯', a, items)
        time.sleep(1)


if __name__ == '__main__':
    # 是Thread方法創建新的線程,將需要創建新線程的函數用目標(target)參數傳入
    demo1 = threading.Thread(target=eat, name='我是線程一', args=(x, ), kwargs={'items': lis})
    # 啓動線程
    demo1.start()
    print(x, lis)

共享全局變量資源競爭(鎖)

當多線程中如果發生資源搶佔,新版Python使用的是一套時間輪方式進行資源分配
當多個線程都需要用到同一個變量時,這時候如果需要保證這個變量能在一個線程中擁有最高的優先級,需要此線程控制變量時,不能讓其他線程改變變量,這時候我們就需要用到.Lock鎖或.RLock遞歸鎖。
在使用中,鎖與遞歸鎖最大的不同爲:鎖只能加單層,而遞歸鎖能加多層鎖(遞歸鎖解鎖的層數需要與加鎖相同)
在不加鎖的情況下,運行下列代碼,我們會發現兩個線程都會對共享的全局變量進行修改。

import threading
import time
x = 3
lis = [1, 2, 3]


# 創建多線程的函數
def eat(a, items):
    for i in range(3):
        a += 1
        items.append(a)
        print('我在喫飯', lis)
        time.sleep(1)


def code(a, items):
    for i in range(3):
        a += 1
        items.append(a)
        print('我在敲碼', lis)
        time.sleep(1.01)
        

if __name__ == '__main__':
    # 是Thread方法創建新的線程,將需要創建新線程的函數用目標(target)參數傳入
    demo1 = threading.Thread(target=eat, name='我是線程一', args=(x, ), kwargs={'items': lis})
    demo2 = threading.Thread(target=code, args=(x, ), kwargs={'items': lis})
    # 啓動線程
    demo1.start()
    demo2.start()

在這裏插入圖片描述

互斥鎖

我們當前我們需要一個線程獨佔此資源進行修改的時候,我們就要用到鎖。
我們先需要實例化一個鎖.threading.Lock(),然後在需要加鎖的地方調用實例化互斥鎖的.acquire()方法,解鎖用.release()方法。(如果需要多層鎖實例化的是遞歸鎖RLock,則加鎖次數需要和解鎖次數相同才能完全解鎖)

import threading
import time
x = 3
lis = [1, 2, 3]
# 實例化鎖
mylock = threading.Lock()


# 創建多線程的函數
def eat(a, items):
    mylock.acquire()
    for i in range(3):
        a += 1
        items.append(a)
        print('我在喫飯', lis)
        time.sleep(1)
    mylock.release()


def code(a, items):
    mylock.acquire()
    for i in range(3):
        a += 1
        items.append(a)
        print('我在敲碼', lis)
        time.sleep(1.01)
    mylock.release()


if __name__ == '__main__':
    # 是Thread方法創建新的線程,將需要創建新線程的函數用目標(target)參數傳入
    demo1 = threading.Thread(target=eat, name='我是線程一', args=(x, ), kwargs={'items': lis})
    demo2 = threading.Thread(target=code, args=(x, ), kwargs={'items': lis})
    # 啓動線程
    demo1.start()
    demo2.start()

在這裏插入圖片描述

死鎖

在線程間共享多個資源的時候,如果兩個線程分別佔有一部分資源並且同時等待對方的資源,就會造成死鎖。
比如說現有A、B兩個變量,我們創建一個線程加鎖調用了A變量,又創建了一個線程加鎖調用B變量,然後調用A變量的線程現在需要調用B變量,等調用結束纔會結束,而當前調用了B變量的線程需要調用A變量,等結束了纔會解鎖,這時候就形成了死鎖。

import threading
import time
x = 3
y = 0
lis = [1, 2, 3]
# 實例化鎖
mylocka = threading.Lock()
mylockb = threading.Lock()


# 創建多線程的函數
def eat(a, b):
    mylocka.acquire()
    print(a)
	# 適當給些延遲讓另一個線程能順利啓動    
    time.sleep(1)
    mylockb.acquire()
    print(b)
    mylockb.release()
    mylocka.release()


def code(a, b):
    mylockb.acquire()
    print(b)
    mylocka.acquire()
    print(a)
    mylocka.release()
    mylockb.release()


if __name__ == '__main__':
    # 是Thread方法創建新的線程,將需要創建新線程的函數用目標(target)參數傳入
    demo1 = threading.Thread(target=eat, args=(x, y))
    demo2 = threading.Thread(target=code, args=(x, y))
    # 啓動線程
    demo1.start()
    demo2.start()

在程序設計的時候儘量避免死鎖,一旦導致死鎖,可能會導致系統資源被大量消耗,導致系統崩潰等嚴重後果。還可以考慮加入超時等待,來主動跳出死鎖。

Python守護線程

在多線程中只要還有一個非守護線程線程存活,那麼我們的程序就不會結束,主線程默認爲非守護線程,所以繼承與主線程的所以子線程默認也爲非守護線程,我們可以改變daemon屬性的值來改變是否爲守護線程。當主線程執行完畢後,剩下的線程如果沒有非守護線程,那麼程序就會退出。
就哪上述死鎖的案例來說,我們如果將兩個會行程死鎖的線程設置成爲守護線程,當主線程執行完畢後,已經被設置成守護線程的死鎖子線程就不會影響程序的退出。

import threading
import time
x = 3
y = 0
lis = [1, 2, 3]
# 實例化鎖
mylocka = threading.Lock()
mylockb = threading.Lock()


# 創建多線程的函數
def eat(a, b, items):
    mylocka.acquire()
    print(a)
    time.sleep(1)
    mylockb.acquire()
    print(b)
    mylockb.release()
    mylocka.release()


def code(a, b, items):
    mylockb.acquire()
    print(b)
    mylocka.acquire()
    print(a)
    mylocka.release()
    mylockb.release()
    time.sleep(1)


if __name__ == '__main__':
    # 是Thread方法創建新的線程,將需要創建新線程的函數用目標(target)參數傳入
    demo1 = threading.Thread(target=eat, args=(x, y),)
    demo2 = threading.Thread(target=code, args=(x, y), daemon=True)
    # 啓動線程
    demo1.daemon = True
    demo1.start()
    demo2.start()

手動添加阻塞進程

多進程中還有一個比較常用的方法是.join(),用於等待子進程執行完成
如下圖所示,當給demo1添加.join()後,會阻塞demo2的運行。當demo1運行完成後demo2纔會開始運行。
在這裏插入圖片描述

Python多進程

多進程在Python中顯得至關重要,因爲CPython的GIL的存在Python,導致Python在多線程時同時只會有一個線程在運行,所以無法很好的利用多處理器的優勢。但如果我們使用多個進程,那麼他們的GIL也是獨立存在的,這樣我們在處理計算密集型任務的時候就可以更加高效的發揮出多核處理器的性能。

GIL(Global Interpreter Lock 全局解釋器鎖)CPython 解釋器所採用的一種機制,它確保同一時刻只有一個線程在執行 Python bytecode。此機制通過設置對象模型(包括 dict 等重要內置類型)針對併發訪問的隱式安全簡化了 CPython 實現。給整個解釋器加鎖使得解釋器多線程運行更方便,其代價則是犧牲了在多處理器上的並行性。
不過,某些標準庫或第三方庫的擴展模塊被設計爲在執行計算密集型任務如壓縮或哈希時釋放 GIL。此外,在執行 I/O 操作時也總是會釋放 GIL。
創建一個(以更精細粒度來鎖定共享數據的)“自由線程”解釋器的努力從未獲得成功,因爲這會犧牲在普通單處理器情況下的性能。據信克服這種性能問題的措施將導致實現變得更復雜,從而更難以維護。
簡述:一個時刻只有一個線程可以執行Python代碼

創建多進程

多進程在基礎用法上和多線程十分類似,只是庫名有所區別,多進程的庫爲multiprocessing,

import time
import multiprocessing
import random


def eat():
    for i in range(3):
        print('我在喫飯')
        time.sleep(1)


def code():
    for i in range(3):
        print('我在擼碼')
        time.sleep(1)


def bilibili():
    for i in range(3):
        print('我在看番')
        time.sleep(1)


def main():
    demo1 = multiprocessing.Process(target=eat)
    demo2 = multiprocessing.Process(target=code)
    demo3 = multiprocessing.Process(target=bilibili)
    demo1.start()
    demo2.start()
    demo3.start()


if __name__ == '__main__':
    main()

當上述代碼運行任務管理器中會出現了五個Python進程,Pycharm會產生兩個進程,還有三個則爲我們生成的子進程。
在這裏插入圖片描述
多進程的multiprocessing.Process()方法中和多線程的threading.Thread()方法中使用的屬性相同,並且功能類似。很多在多線程threading庫中存在的方法在在multiprocessing庫中也同樣存在。

多進程交換對象與共享資源(進程通信)

多進程不同於多線程使用的所有資源都爲共享資源,Python每創建一個進程都可以看做Python將代碼重新進行了複製粘貼。所以每個進程之中的資源並不是共享的,在使用多進程時儘量避免共享資源的使用,不過無論是對象,還是變量,都有對應的方法可以進行資源共享。

隊列Queue()

這裏的隊列就是數據結構中的隊列,在這裏就不在贅述,如果有不熟悉隊列可以查看我這篇博客通過Python創建隊列。隊列的單向結構也標註着隊列在多進程中交互對象是單向的。
注意:多線程中的隊列不同於普通隊列,線程中用於交互對象的隊列不能爲queue.Queue,必須是multiprocessing.Queue隊列中需要傳入指定一個長度(默認長度根據系統自動設定),如果超過長度繼續添加程序會被阻塞。

Queue常用方法名 作用
put 向隊列中添加元素,如果隊列已滿則線程進入堵塞狀態
get 從隊列中取出元素,如果隊列爲空則線程進入堵塞狀態
full 判斷隊列是否已滿
empty 判斷隊列是否爲空
qsize 判斷當前隊列中有多少元素
close 情況隊列
put_nowait 向隊列中添加元素,如果隊列已滿則返回異常並不堵塞
get_nowait 從隊列中取出元素,如果隊列爲空則返回異常並不堵塞
import time
import multiprocessing


def eat(q):
    for i in range(3):
        q.put(i)
        print('我在喫飯')
        time.sleep(1)


def code(q):
    for i in range(3):
        print('我在擼碼', q.get())
        time.sleep(1)


def main():
    # 定義一個用於共享數據的隊列
    q = multiprocessing.Queue(3)
    demo1 = multiprocessing.Process(target=eat, args=(q,))
    demo2 = multiprocessing.Process(target=code, args=(q,))
    demo1.start()
    demo1.join()
    demo2.start()

隊列是線程和進程安全的。

管道Pipe()

不同於隊列,管道默認是雙向的。

請注意,如果兩個進程(或線程)同時嘗試讀取或寫入管道的 同一 端,則管道中的數據可能會損壞。當然,在不同進程中同時使用管道的不同端的情況下不存在損壞的風險。

當實例化管道時,會已元組的形式返回兩個對象,分別爲管道的首尾。
我在windows環境下測試管道發現會出現很多異常,而且存在數據損壞的風險,所以除非必要,否則不建議使用管道。
管道的添加與讀取元素分別爲send()recv()

import time
import multiprocessing


def eat(p):
    for i in range(3):
        p.send(i)
        print('我在喫飯')
        time.sleep(1)


def code(p):
    for i in range(3):
        print('我在擼碼', p.recv())
        time.sleep(1)


def main():
    # 定義一個用於共享數據的管道
    head_p, foot_p = multiprocessing.Pipe()
    demo1 = multiprocessing.Process(target=eat, args=(head_p,))
    demo2 = multiprocessing.Process(target=code, args=(foot_p,))
    demo1.start()
    demo1.join()
    demo2.start()


if __name__ == '__main__':
    main()

共享內存

在多進程multiprocessing中同樣支持鎖,且使用方式與上述線程一致,再次就不做贅述了。
在多進程中如果需要共享變量我們可以使用ValueArray,Value爲單個對象,而 Array爲數組類型共享內存。
而且ValueArray都需要傳入一個代碼類型或類型(typecode_or_type),同時Array還需傳入一個默認數組

常用代碼類型或類型 含義
'b' 整數型
'd' 浮點型
'u' 字符型
import time
import multiprocessing


def eat(v, a):
    for i in range(3):
        v.value = a[i]
        a[i] = -a[i]
        print('我在喫飯', v.value, a[i])
        time.sleep(1)


def code(v, a):
    for i in range(3):
        print('我在擼碼', v.value, a[i])
        time.sleep(1.01)


def main():
    # 定義一個共享對象
    v = multiprocessing.Value('b')
    a = multiprocessing.Array('b', range(3))
    demo1 = multiprocessing.Process(target=eat, args=(v, a))
    demo2 = multiprocessing.Process(target=code, args=(v, a))
    demo1.start()
    demo2.start()


if __name__ == '__main__':
    main()

進程池

當存在一個正在運行的進程時,需要耗費大量資源,所以無休無止的創建進程無疑嚴重影響計算機性能,而進程池就是爲解決這個問題而產生的。
在這裏插入圖片描述
我們可以設置一個工作進程數目,如果當前創建進程時,沒有到達工作進程數目,則立馬創建,如果工作進程數目已滿,則等待有進程關閉後在將待創建的進程創建。

Pool([processes[, initializer[, initargs[, maxtasksperchild[, context]]]]]
processes 是要使用的工作進程數目。如果 processes 爲 None,則使用 os.cpu_count() 返回的值(os.cpu_count() 爲系統中cpu的數量)。
如果 initializer 不爲 None,則每個工作進程將會在啓動時調用 initializer(*initargs)。
maxtasksperchild 是一個工作進程在它退出或被一個新的工作進程代替之前能完成的任務數量,爲了釋放未使用的資源。默認的 maxtasksperchild 是 None,意味着工作進程壽與池齊。
context 可被用於指定啓動的工作進程的上下文。通常一個進程池是使用函數 multiprocessing.Pool() 或者一個上下文對象的 Pool() 方法創建的。在這兩種情況下, context 都是適當設置的。

在Pool中我們最常會用到的是processes 參數,我們一般會指定線程池的大小。向線程池中添加線程的方法有兩種分別是.apply().apply_async()方法

  1. apply(func[, args[, kwds]]):使用此方法會造成阻塞,當主線程執行到此時會立即啓用此進程,然後等待此進程執行完畢後在繼續往下執行(使用此方法會導致程序始終只會有一個進程在運行,導致程序無法並行),官方建議棄用此方法
  2. apply_async(func[, args[, kwds[, callback[, error_callback]]]]):非阻塞運行,進程被添加到進程池後,進程池會啓動進程並且準備就緒,等待系統調度運行。(如果主進程運行結束,則會導致進程池關閉,未被調度運行的進程會被清除)
    • func:傳入的函數或方法
    • args與kwds:相當於.Process()中的args與kwargs
    • callback:它必須是一個接受單個參數的可調用對象。當執行成功時, callback 會被用於處理執行後的返回結果,否則,調用 error_callback 。
    • error_callback:它必須是一個接受單個參數的可調用對象。當目標函數執行失敗時, 會將拋出的異常對象作爲參數傳遞給 error_callback 執行。
  3. close():阻止後續任務提交到進程池,當所有任務執行完成後,工作進程會退出。(這裏的close並不是清除或者關閉進程池的意思)
  4. terminate():不必等待未完成的任務,立即停止工作進程。當進程池對象唄垃圾回收時, 會立即調用 terminate() 。
  5. join():等待工作進程結束。
import time
import multiprocessing

def eat(v, a):
    print('我在喫飯', v, a)
    time.sleep(random.random())
    return 'eat執行結束'


def code(v, a):
    print('我在擼碼', code出現bug)
    time.sleep(random.random())


def callback(data):
    print(data)


def main():
    # 定義一個只能同時運行三個進程的進程池
    po = multiprocessing.Pool(3)

    for i in range(10):
        po.apply_async(eat, (i, ), {'a': '我是關鍵字傳參'}, callback, callback)
        po.apply_async(code, (i, '0'))
    print("--開始運行--")
    po.close()

    po.join()
    print("--結束運行--")


if __name__ == '__main__':
    main()

在這裏插入圖片描述
因爲這裏我們使用的是.apply_async()方法,主進程不會主動等待子進程,所以當我們將所以進程添加到進程池後使用.close()方法關閉進程添加通道,然後使用.join()方法讓主進程停下等待子進程完全結束後在結束。

進程池間的進程通信(服務進程)

進程池使用常規的多進程方式是無法正常通信的,這時候我們就需要服務進程Manager()的幫助
Manager() 返回的管理器支持類型: list 、 dict 、 Namespace 、 Lock 、 RLock 、 Semaphore 、 BoundedSemaphore 、 Condition 、 Event 、 Barrier 、 Queue 、 Value 和 Array 。Manager() 中調用的類似和原類型使用方式一致,他只是使這些方式能在進程池中正常工作,下面我們就哪Queue來演示.Manager().Queue()的使用方式

import time
import multiprocessing
import random


def eat(v, a):
    for i in range(1):
        v.put(i)
        print('我在喫飯', a)
        time.sleep(1)
    return 'eat執行結束'


def code(v, a):
    for i in range(1):
        print('我在擼碼', v.get())
        time.sleep(1)


def callback(data):
    print(data)


def main():
    # 使用服務進程(Manager方法)定義一個隊列
    q = multiprocessing.Manager().Queue(3)
    # 定義一個只能同時運行三個進程的進程池
    po = multiprocessing.Pool(3)

    for i in range(10):
        po.apply_async(eat, (q, ), {'a': '我是關鍵字傳參'}, callback, callback)
        po.apply_async(code, (q, '0'), callback=callback, error_callback=callback)
    print("--開始運行--")
    po.close()

    po.join()
    print("--結束運行--")


if __name__ == '__main__':
    main()

協程

協程,又稱微線程。是一種以極小資源就可以進行多任務的方法。也就是在單個線程中實現多任務。協程是用單個線程實現程序的異步非阻塞處理。

進程、線程、協程多任務對比

  • 進程: 多核兼容性好,併發情況相對較多,佔用資源大,進程之間切換成本高
  • 線程: 多核兼容性一般,因爲Python的GIL的存在,只有在特殊情況在會用到多核,佔用資源較少,線程之間切換成本較低
  • 協程: 只能單線程運行,不兼容多核,不會佔用額外資源,切換成本最低。

協程的使用

協程的出現是由生成器(yield)一步步進化而來,生成器我在之前的一篇博客中已經進行了詳細的描述,在這裏就不在贅述Python的生成器
Python在Python3.5之後引入了:asyncawait語法實現協程(在Python3.7中全面推廣,並且將其設爲關鍵字),在舊版的Python中也可以下載第三方的greenletgevent下載使用(pip install greenletpip install gevent)因爲舊版已經逐漸被淘汰,下面我們會使用官方的async、await進行協程的說明已經使用。
使用協程我們需要引入asyncio這個庫。
注:asyncawait語法在Python3.7後爲關鍵字,屬於Python的重要語法,但是大多時候我們都需要asyncio這個庫裏面的一些方法才能進行協程。

import asyncio


# 使用async語法創建的對象爲協程
async def eat():
    while True:
        # await用於指定可等待的對象(await指定的可等待對象必須包含__await__方法)
        await asyncio.sleep(1)
        print('開飯了')
# 如果直接調用eat()函數,不會有任何作用,因爲創建好的eat()沒有被等待
eat()
# 使用asyncio的run方法即可啓動協程中可等待對象
asyncio.run(eat())

協程中最基礎的async是用來創建協程的,而await用來標明可等待對象

asyncio用法

常用的asyncio方法 作用
asyncio.sleep(delay, result=None, *, loop=None) 用於線程睡眠(time.sleep()不可作爲替代品)
asyncio.run(coro, *, debug=False) 啓動協程並返回coro結果
asyncio.create_task(coro, *, name=None) 創建任務(coro爲協程,Python3.8之後支持給協程命名)
asyncio.gather(*aws, loop=None, return_exceptions=False) 併發 運行 aws 序列中的 可等待對象。最後程序會以列表的形式返回所有運行的協程值。(loop爲指定循環順序,return_exceptions爲是否跳過報錯協程,默認不會跳過,如果改成True則會將報錯信息同以返回值的形式返回)

創建多任務

使用協程創建多任務如果是少量,我們可以使用create_task方法逐個創建並且啓用,如果有大量需要創建協程的對象,我們可以使用gather方法一次性全部添加使用

import asyncio


# 使用async方法創建的對象爲協程
async def eat(n):
    for i in range(n):
        # await用於指定可等待的對象(await指定的可等待對象必須包含__await__方法)
        await asyncio.sleep(n-i)
        print('開飯了')
# 使用asyncio的run方法即可啓動協程
# asyncio.run(eat())


async def code(n):
    for i in range(n):
        await asyncio.sleep(i)
        print('擼碼了')


async def main():
    # 使用create_task創建一個
    dome1 = asyncio.create_task(eat(3))
    dome2 = asyncio.create_task(code(3))
    print('--開始執行--')
    await dome1
    await dome2
    print('--結束執行--')


asyncio.run(main())

上述程序中使用的create_task方法可以用gather方法代替

    # dome1 = asyncio.create_task(eat(3))
    # dome2 = asyncio.create_task(code(3))
    # await dome1
    # await dome2
    # 以上代碼相當於
	await asyncio.gather(eat(3), code(3))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章