豬行天下之Python基礎——9.2 Python多執行緒與多程式(中)

coder-pig發表於2019-04-13

內容簡述:

  • 1、threading模組詳解
  • 2、queue模組詳解

1、threading模組詳解

Python提供的與執行緒操作相關的模組,網上有很多資料還是用的thread模組,在3.x版本中已經使用threading來替代thread,如果你在python 2.x版本想使用threading的話,可以使用dummy_threading模組


① threading模組提供的可直接呼叫的函式

  • active_count():獲取當前活躍(alive)執行緒的個數。
  • current_thread():獲取當前的執行緒物件。
  • get_ident():返回當前執行緒的索引,一個非零的整數(3.3新增)。
  • enumerate():獲取當前所有活躍執行緒的列表。
  • main_thread():返回主執行緒物件(3.4新增)。
  • settrace(func):設定一個回撥函式,在run()執行之前被呼叫。
  • setprofile(func):設定一個回撥函式,在run()執行完畢之後呼叫。
  • stack_size():返回建立新執行緒時使用的執行緒堆疊大小。
  • threading.TIMEOUT_MAX:堵塞執行緒時間最大值,超過這個值會棧溢位。

② 執行緒區域性變數(Thread-Local Data)

問題引入

在一個程式內所有的執行緒共享程式的全域性變數,執行緒間共享資料很方便但是每個執行緒都可以隨意修改全域性變數,可能會引起執行緒安全問題。

解決方法

對於這種執行緒私有資料,最簡單的方法就是對變數加鎖或使用區域性變數,只有執行緒自身可以訪問,其他執行緒無法訪問。除此之外還可以使用threading模組為我們提供的ThreadLocal變數,它本身是一個全域性變數,但是執行緒們卻可以使用它來儲存私有資料。

用法簡介

定義一個全域性變數:data = thread.local(),然後就可以往裡面存資料啦,比如data.num = xxx,但是有一點要注意:如果data裡沒有設定對應的屬性,直接取會報AttributeError異常,使用時可以捕獲這個異常或先呼叫hasattr(物件,屬性)判斷物件中是否有該屬性!使用程式碼示例如下:

import threading
import random

data = threading.local()

def show(d):
    try:
        num = d.num
    except AttributeError:
        print("執行緒 %s 還未設定該屬性!" % threading.current_thread().getName())
    else:
        print("執行緒 %s 中該屬性的值為 = %s" % (threading.current_thread().getName(), num))

def thread_call(d):
    show(d)
    d.num = random.randint(1100)
    show(d)

if __name__ == '__main__':
    show(data)
    data.num = 666
    show(data)
    for i in range(2):
        t = threading.Thread(target=thread_call, args=(data,), name="Thread " + str(i))
        t.start()
複製程式碼

執行結果如下

執行緒 MainThread 還未設定該屬性!
執行緒 MainThread 中該屬性的值為 = 666
執行緒 Thread 0 還未設定該屬性!
執行緒 Thread 0 中該屬性的值為 = 80
執行緒 Thread 1 還未設定該屬性!
執行緒 Thread 1 中該屬性的值為 = 17
複製程式碼

不同執行緒訪問這個ThreadLocal變數,返回的都是不一樣的值,原理:

threading.local()例項化一個全域性物件,這個全域性物件裡有一個大字典,鍵值為兩個弱引用物件{執行緒物件,字典物件},然後可以通過current_thread()獲得當前的執行緒物件,然後根據這個物件可以拿到對應的字典物件,然後進行引數的讀或者寫。


③ 執行緒物件(threading.Thread)

建立新執行緒的兩種方式

  • 1.直接建立threading.Thread物件並把呼叫物件作為引數傳入
  • 2.繼承threading.Thread類重寫run()方法

使用程式碼示例(驗證單執行緒快還是多執行緒快):

import threading
import time

def catch_fish():
    pass

def one_thread():
    start_time = time.time()
    for i in range(11001):
        catch_fish()
    end_time = time.time()
    print("單執行緒測試 耗時 === %s" % str(end_time - start_time))

def muti_thread():
    start_time = time.time()
    for i in range(11001):
        threading.Thread(target=catch_fish()).start()
    end_time = time.time()
    print("多執行緒測試 耗時 === %s" % str(end_time - start_time))

if __name__ == '__main__':
    # 單執行緒
    threading.Thread(one_thread()).start()
    # 多執行緒
    muti_thread()
複製程式碼

執行結果如下:

單執行緒測試 耗時 === 0.00011301040649414062
多執行緒測試 耗時 === 0.07665514945983887
複製程式碼

從輸出結果可以看到,多執行緒反而比單執行緒要慢,原因是前面介紹過的Python中的全域性直譯器鎖(GIL), 使得任何時候僅有一個執行緒在執行。

Thread類建構函式

def __init__(self, group=None, target=None, name=None,
                 args=(), kwargs=None, *, daemon=None)
:

複製程式碼

建構函式引數依次是

  • group:執行緒組
  • target:要執行的函式
  • name:執行緒名字
  • args/kwargs:要傳入的函式的引數
  • daemon:是否為守護執行緒

相關屬性與函式

  • start():啟動執行緒,只能呼叫一次
  • run():執行緒執行的操作,可繼承Thread重寫,引數可從args和kwargs獲取;
  • join([timeout]):堵塞呼叫執行緒,直到被呼叫執行緒執行結束或超時;如果
    沒設定超時時間會一直堵塞到被呼叫執行緒結束。
  • name/getName():獲得執行緒名;
  • setName():設定執行緒名;
  • ident:執行緒是已經啟動,未啟動會返回一個非零整數;
  • is_alive():判斷是否在執行,啟動後,終止前;
  • daemon/isDaemon():執行緒是否為守護執行緒;
  • setDaemon():設定執行緒為守護執行緒;

④ Lock(指令鎖)與RLock(可重入鎖)

在概念那裡就講了,多個程式併發的訪問臨界資源可能會引起執行緒同步安全問題,寫個簡單的例子,然後再引入同步鎖。程式碼示例如下:

import threading

file_name = "test.txt"

# 定義一個寫入檔案的方法
def write_to_file(msg):
    try:
        with open(file_name, "a+", encoding="utf-8"as f:
            f.write(msg + "\n")
    except OSError as reason:
        print(str(reason))

class MyThread(threading.Thread):
    def __init__(self, msg):
        super().__init__()
        self.msg = msg
    def run(self):
        write_to_file(self.name + "~" + self.msg)

if __name__ == '__main__':
    for i in range(121):
        t = MyThread(str(i)).start()
複製程式碼

執行結果如下

# test.txt檔案內容
Thread-1~1
Thread-5~5
Thread-3~3
Thread-2~2
Thread-4~4
Thread-6~6
Thread-7~7
Thread-8~8
Thread-10~10
Thread-9~9
Thread-11~11
Thread-13~13
Thread-12~12
Thread-14~14
Thread-15~15
Thread-16~16
Thread-17~17
Thread-19~19
Thread-20~20
Thread-18~18
複製程式碼

發現結果並沒有按照我們預想的1-20那樣順序列印,而是亂的,threading模組中提供了兩個類來確保多執行緒共享資源的訪問:「Lock」 和 「RLock」。

Lock指令鎖,有兩種狀態(鎖定與非鎖定),以及兩個基本函式:

使用acquire()設定為locked狀態,使用release()設定為unlocked狀態。acquire()函式有兩個可選引數:blocking=True:是否堵塞當前執行緒等待;timeout=None:堵塞等待時間。如果成功獲得lock,acquire返回True,否則返回False,超時也是返回False。使用起來也很簡單,在訪問共享資源的地方acquire一下,用完release就好。使用程式碼示例如下:

import threading

file_name = "test.txt"
lock = threading.Lock()

# 定義一個寫入檔案的方法(加鎖)
def write_to_file(msg):
    if lock.acquire():
        try:
            with open(file_name, "a+", encoding="utf-8"as f:
                f.write(msg + "\n")
        except OSError as reason:
            print(str(reason))
        finally:
            lock.release()

class MyThread(threading.Thread):
    def __init__(self, msg):
        super().__init__()
        self.msg = msg
    def run(self):
        write_to_file(self.name + "~" + self.msg)

if __name__ == '__main__':
    for i in range(1101):
        t = MyThread(str(i)).start()
複製程式碼

這裡把迴圈次數改成了101,反覆執行多次,test.txt中寫入順序也是正確的,加鎖有效。另外有一點要注意:如果鎖的狀態是unlocked,此時呼叫release會丟擲RuntimeError異常!

RLock可重入鎖,和Lock類似,但RLock卻可以被同一個執行緒請求多次! 舉個例子:在一個執行緒裡呼叫Lock物件的acquire方法兩次。

lock.acquire()
lock.acquire()
lock.release()
lock.release()
複製程式碼

你會發現程式卡住不動,因為已經發生了死鎖,但是方法呼叫是在同一個執行緒裡的,這很不合理吧。這個時候就可以引入RLock了,使用RLock編寫一樣程式碼,只需把threading.Lock()改成threading.RLock(),即可解決這個問題。

雖然使用RLock可以規避同一個執行緒引起的死鎖問題,但是acquire和release函式要成對出現,即有多少個acquire就要有多少個release,才能夠正真釋放鎖


⑤ 條件變數(Condition)

上面的互斥鎖Lock和RLock只是最簡單的同步機制,Python為我們提供了Condition(條件變數),以便於處理複雜執行緒同步問題,比如最經典的生產者與消費者問題。Condition除了提供與Lock類似的acquire()與release()函式外,還提供了wait()與notify()函式。

用法簡介

  • 1.呼叫`threading.Condition`獲得一個條件變數物件;
  • 2.執行緒呼叫acquire獲得Condition物件
  • 3.進行條件判斷,不滿足條件呼叫wait函式,滿足條件,進行一些處理改變條件後,呼叫notify函式通知處於wait狀態的執行緒,重新進行條件判斷。

程式碼示例如下(實現一個簡單的消費者和生產者):

import threading
import time

condition = threading.Condition()
products = 0  # 商品數量

# 定義生產者執行緒類
class Producer(threading.Thread):
    def run(self):
        global products
        while True:
            if condition.acquire():
                if products >= 99:
                    condition.wait()
                else:
                    products += 2
                    print(self.name + "生產了2個產品,當前剩餘產品數為:" + str(products))
                    condition.notify()
                condition.release()
                time.sleep(2)

# 定義消費者執行緒類
class Consumer(threading.Thread):
    def run(self):
        global products
        while True:
            if condition.acquire():
                if products < 4:
                    condition.wait()
                else:
                    products -= 4
                    print(self.name + "消耗了4個產品,當前剩餘產品數為:" + str(products))
                    condition.notify()
            condition.release()
            time.sleep(2)

if __name__ == '__main__':
    # 建立五個生產者執行緒
    for i in range(5):
        p = Producer()
        p.start()
    # 建立兩個消費者執行緒
    for j in range(2):
        c = Consumer()
        c.start()
複製程式碼

部分執行結果如下

Thread-1生產了2個產品,當前剩餘產品數為:2
Thread-2生產了2個產品,當前剩餘產品數為:4
Thread-3生產了2個產品,當前剩餘產品數為:6
Thread-4生產了2個產品,當前剩餘產品數為:8
Thread-5生產了2個產品,當前剩餘產品數為:10
Thread-6消耗了4個產品,當前剩餘產品數為:6
Thread-7消耗了4個產品,當前剩餘產品數為:2
Thread-1生產了2個產品,當前剩餘產品數為:4
Thread-5生產了2個產品,當前剩餘產品數為:6
Thread-3生產了2個產品,當前剩餘產品數為:8
Thread-7消耗了4個產品,當前剩餘產品數為:4
Thread-6消耗了4個產品,當前剩餘產品數為:0
Thread-4生產了2個產品,當前剩餘產品數為:2
複製程式碼

Condition維護著一個互斥鎖物件(預設是RLock),也可以自己例項化一個在Condition例項化的時候通過建構函式傳入,所以,呼叫的Condition的acquire與release函式,其實呼叫就是這個鎖物件的acquire與release函式。

Condition提供的其他函式

  • wait(timeout=None):釋放鎖,同時執行緒被掛起,直到收到通知被喚醒
    或超時(如果設定了timeout),當執行緒被喚醒並重新佔有鎖時,程式才繼續執行;
  • wait_for(predicate, timeout=None):等待知道條件為True,predicate應該是
    一個回撥函式,返回布林值,timeout用於指定超時時間,返回值為回撥函式返回
    的布林值,或者超時,返回False(3.2新增);
  • notify(n=1):預設喚醒一個正在的等待執行緒,notify並不釋放鎖!!!
  • notify_all():喚醒所有等待執行緒,進入就緒狀態,等待獲得鎖,notify_all 同樣不釋放鎖!!!

注:上述函式只有在acquire之後才能呼叫,不然會報RuntimeError異常。


⑥ 訊號量(Semaphore)

訊號量,也是一個簡單易懂的東西,舉個形象的例子:

廁所裡有五個坑位,每有個人去廁所就會佔用一個坑位,所剩餘的坑位-1,當五個坑都被人佔滿時,新來的人就只能在外面等候,直到有人出來為止。這裡的五個坑位就是訊號量,蹲坑的人就是執行緒,初始值為5,來人-1,走人+1,超過最大值,新來的處於堵塞狀態,我們寫下程式碼來還原這個過程。

訊號量使用程式碼示例如下

import threading
import time
import random

s = threading.Semaphore(5)  # 糞坑

class Human(threading.Thread):
    def run(self):
        s.acquire()  # 佔坑
        print("蹲坑 - " + self.name + " - " + str(time.ctime()))
        time.sleep(random.randrange(13))
        print("走人 - " + self.name + " - " + str(time.ctime()))
        s.release()  # 走人

if __name__ == '__main__':
    for i in range(10):
        human = Human()
        human.start()
複製程式碼

執行結果如下

蹲坑 - Thread-1 - Tue Jul 17 19:59:15 2018
蹲坑 - Thread-2 - Tue Jul 17 19:59:15 2018
蹲坑 - Thread-3 - Tue Jul 17 19:59:15 2018
蹲坑 - Thread-4 - Tue Jul 17 19:59:15 2018
蹲坑 - Thread-5 - Tue Jul 17 19:59:15 2018
走人 - Thread-1 - Tue Jul 17 19:59:16 2018
蹲坑 - Thread-6 - Tue Jul 17 19:59:16 2018
走人 - Thread-2 - Tue Jul 17 19:59:16 2018
走人 - Thread-3 - Tue Jul 17 19:59:16 2018
蹲坑 - Thread-8 - Tue Jul 17 19:59:16 2018
走人 - Thread-5 - Tue Jul 17 19:59:16 2018
蹲坑 - Thread-7 - Tue Jul 17 19:59:16 2018
蹲坑 - Thread-9 - Tue Jul 17 19:59:16 2018
走人 - Thread-4 - Tue Jul 17 19:59:17 2018
蹲坑 - Thread-10 - Tue Jul 17 19:59:17 2018
走人 - Thread-6 - Tue Jul 17 19:59:17 2018
走人 - Thread-8 - Tue Jul 17 19:59:17 2018
走人 - Thread-9 - Tue Jul 17 19:59:17 2018
走人 - Thread-7 - Tue Jul 17 19:59:18 2018
走人 - Thread-10 - Tue Jul 17 19:59:19 2018
複製程式碼

⑦ 通用的條件變數(Event)

Python提供的「用於執行緒間通訊的訊號標誌」,一個執行緒標識了一個事件,其他執行緒處於等待狀態,直到事件發生後,所有執行緒都會被啟用。Event物件屬性實現了簡單的執行緒通訊機制,提供了設定訊號,清除訊號,等待等用於實現執行緒間的通訊。提供以下四個可供呼叫的方法:

  • is_set():判斷內部標誌是否為真
  • set():設定訊號標誌為真
  • clear():清除Event物件內部的訊號標誌(設定為false)
  • wait(timeout=None):使執行緒一直處於堵塞,知道識別符號變為True

使用程式碼示例(汽車等紅綠燈的例子):

import threading
import time
import random

class CarThread(threading.Thread):
    def __init__(self, event):
        threading.Thread.__init__(self)
        self.threadEvent = event
    def run(self):
        # 休眠模擬汽車先後到達路口時間
        time.sleep(random.randrange(110))
        print("汽車 - " + self.name + " - 到達路口...")
        self.threadEvent.wait()
        print("汽車 - " + self.name + " - 通過路口...")

if __name__ == '__main__':
    light_event = threading.Event()
    # 假設有20臺車子
    for i in range(20):
        car = CarThread(event=light_event)
        car.start()
    while threading.active_count() > 1:
        light_event.clear()
        print("紅燈等待...")
        time.sleep(3)
        print("綠燈通行...")
        light_event.set()
        time.sleep(2)
複製程式碼

執行結果如下

紅燈等待...
汽車 - Thread-10 - 到達路口...
汽車 - Thread-14 - 到達路口...
汽車 - Thread-9 - 到達路口...
汽車 - Thread-11 - 到達路口...
汽車 - Thread-12 - 到達路口...
綠燈通行...
汽車 - Thread-11 - 通過路口...
汽車 - Thread-10 - 通過路口...
汽車 - Thread-9 - 通過路口...
汽車 - Thread-14 - 通過路口...
汽車 - Thread-12 - 通過路口...
汽車 - Thread-6 - 到達路口...
汽車 - Thread-6 - 通過路口...
複製程式碼

⑧ 定時器(Timer)

和Thread類似,只是要等待一段時間後才會開始執行,單位秒,用法也很簡單,
程式碼示例如下

import threading
import time

def skill_ready():
    print("菜餚製作完成!!!")

if __name__ == '__main__':
    t = threading.Timer(5, skill_ready)
    t.start()
    while threading.active_count() > 1:
        print("======菜餚製作中======")
        time.sleep(1)
複製程式碼

執行結果如下

======菜餚製作中======
======菜餚製作中======
======菜餚製作中======
======菜餚製作中======
======菜餚製作中======
菜餚製作完成!!!
複製程式碼

⑨ 柵欄(Barrier)

Barrier直譯柵欄,感覺不怎麼好理解,我們可以把它看做是賽馬用的柵欄,然後馬(執行緒)依次來到柵欄前等待(wait),直到所有的馬都停在柵欄面前了,然後所有馬開始同時出發(start)。簡單點說就是: 多個執行緒間的相互等待,呼叫了wait()方法的執行緒進入堵塞, 直到所有的執行緒都呼叫了wait()方法,然後所有執行緒同時進入就緒狀態, 等待排程執行。

建構函式Barrier(parties,action=None,timeout=None)

引數解釋

  • parties:建立一個可容納parties條執行緒的柵欄;
  • action:全部執行緒被釋放時可被其中一條執行緒呼叫的可呼叫物件;
  • timeout:執行緒呼叫wait()方法時沒有顯式設定timeout,就用的這個作為預設值;

相關屬性與函式

  • wait(timeout=None):表示執行緒就位,返回值是一個0到parties-1之間的
    整數, 每條執行緒都不一樣,這個值可以用作挑選一條執行緒做些清掃工作,另外如果
    你在建構函式裡設定了action的話,其中一個執行緒在釋放之前將會呼叫它。如果呼叫
    出錯的話,會讓柵欄進入broken狀態,超時同樣也會進入broken狀態,如果柵欄在
    處於broke狀態的時候呼叫reset函式,會丟擲一個BrokenBarrierError異常。
  • reset():本方法將柵欄置為初始狀態,即empty狀態。所有已經在等待的執行緒都會
    接收到BrokenBarrierError異常,注意當有其他處於unknown狀態的執行緒時,呼叫
    此方法將可能獲取到額外的訪問。因此如果一個柵欄進入了broken狀態, 最好是
    放棄他並新建一個柵欄,而不是呼叫reset方法。
  • abort():將柵欄置為broken狀態。本方法將使所有正在等待或將要呼叫
    wait()方法的執行緒收到BrokenBarrierError異常。本方法的使用情景為,比如:
    有一條執行緒需要abort(),又不想給其他執行緒造成死鎖的狀態,或許設定
    timeout引數要比使用本方法更可靠。
  • parites:將要使用本 barrier 的執行緒的數量
  • n_waiting:正在等待本 barrier 的執行緒的數量
  • broken:柵欄是否為broken狀態,返回一個布林值
  • BrokenBarrierError:RuntimeError的子類,當柵欄被reset()或broken時引發;

使用程式碼示例如下(公司一起去旅遊等人齊才出發):

import threading
import time
import random

class Staff(threading.Thread):
    def __init__(self, barriers):
        threading.Thread.__init__(self)
        self.barriers = barriers

    def run(self):
        print("員工 【" + self.name + "】" + "出門")
        time.sleep(random.randrange(110))
        print("員工 【" + self.name + "】" + "已簽到")
        self.barriers.wait()

def ready():
    print(threading.current_thread().name + ":人齊,出發,出發~~~")

if __name__ == '__main__':
    print("要出去旅遊啦,大家快集合~")
    b = threading.Barrier(10, action=ready, timeout=20)
    for i in range(10):
        staff = Staff(b)
        staff.start()
複製程式碼

執行結果如下

要出去旅遊啦,大家快集合~
員工 【Thread-1】出門
員工 【Thread-2】出門
員工 【Thread-3】出門
員工 【Thread-4】出門
員工 【Thread-5】出門
員工 【Thread-6】出門
員工 【Thread-7】出門
員工 【Thread-8】出門
員工 【Thread-9】出門
員工 【Thread-10】出門
員工 【Thread-8】已簽到
員工 【Thread-4】已簽到
員工 【Thread-5】已簽到
員工 【Thread-6】已簽到
員工 【Thread-9】已簽到
員工 【Thread-2】已簽到
員工 【Thread-3】已簽到
員工 【Thread-7】已簽到
員工 【Thread-1】已簽到
員工 【Thread-10】已簽到
Thread-10:人齊,出發,出發~~~
複製程式碼

2、queue模組詳解

Python中的queue模組中已經實現了一個執行緒安全的多生產者,多消費者佇列,自帶鎖,常用於多執行緒併發資料交換。內建三種型別的佇列:

  • Queue:FIFO(先進先出);
  • LifoQueue:LIFO(後進先出);
  • PriorityQueue:優先順序最小的先出;

三種型別的佇列的建構函式都是(maxsize=0),用於設定佇列容量,如果設定的maxsize小於1,則表示佇列的長度無限長。

兩個異常

  • Queue.Empty:當呼叫非堵塞的get()獲取空佇列元素時會引發;
  • Queue.Full:當呼叫非堵塞的put()滿佇列裡新增元素時會引發;

相關函式

  • size():返回佇列的近似大小,注意:qsize()> 0不保證隨後的get()不會 阻塞也不保證qsize() < maxsize後的put()不會堵塞;
  • empty():判斷佇列是否為空,返回布林值,如果返回True,不保證後續 呼叫put()不會阻塞,同理,返回False也不保證get()呼叫不會被阻塞;
  • full():判斷佇列是否滿,返回布林值如果返回True,不保證後續 呼叫get()不會阻塞,同理,返回False也不保證put()呼叫不會被阻塞;
  • put(item, block=True, timeout=None):往佇列中放入元素,如果block為True且timeout引數為None(預設),為堵塞型put(),如果timeout是 正數,會堵塞timeout時間並引發Queue.Full異常,如果block為False則 為非堵塞put()。
  • put_nowait(item):等價於put(item, False),非堵塞put()
  • get(block=True, timeout=None):移除一個佇列元素,並返回該元素,如果block為True表示堵塞函式,block = False為非堵塞函式,如果設定了timeout,堵塞時最多堵塞超過多少秒,如果這段時間內沒有可用的項,會引發Queue.Empty異常,如果為非堵塞狀態,有資料可用返回資料無資料立即丟擲Queue.Empty異常;
  • get_nowait():等價於get(False),非堵塞get()
  • task_done():完成一項工作後,呼叫該方法向佇列傳送一個完成訊號,任務-1;
  • join():等佇列為空,再執行別的操作;

使用程式碼示例如下

import threading
import queue
import time
import random

work_queue = queue.Queue()

# 任務模擬
def working():
    global work_queue
    while not work_queue.empty():
        data = work_queue.get()
        time.sleep(random.randrange(12))
        print("執行" + data)
        work_queue.task_done()

# 工作執行緒
class WorkThread(threading.Thread):
    def __init__(self, t_name, func):
        self.func = func
        threading.Thread.__init__(self, name=t_name)
    def run(self):
        self.func()

if __name__ == '__main__':
    work_list = []
    for i in range(121):
        work_list.append("任務 %d" % i)
    # 模擬把需要執行的任務放到佇列中
    for i in work_list:
        work_queue.put(i)
    # 初始化一個執行緒列表
    threads = []
    for i in range(0, len(work_list)):
        t = WorkThread(t_name="執行緒" + str(i), func=working)
        t.daemon = True
        t.start()
        threads.append(t)
    work_queue.join()
    for t in threads:
        t.join()
    print("所有任務執行完畢")
複製程式碼

執行結果如下

執行任務 1
執行任務 3
執行任務 5
執行任務 2
執行任務 4
執行任務 6
執行任務 8
執行任務 10
執行任務 13
執行任務 11
執行任務 17
執行任務 18
執行任務 19
執行任務 7
執行任務 14
執行任務 16
執行任務 9
執行任務 15
執行任務 12
執行任務 20
所有任務執行完畢
複製程式碼

如果本文對你有所幫助,歡迎
留言,點贊,轉發
素質三連,謝謝?~


相關文章