天天看點

python多線程結束線程_Python多線程和Office第2部分 僵局 (DEADLOCK) 背鎖 (DESERTED LOCK) 活鎖 (LIVELOCK) 信号 (SEMAPHORE) 比賽條件 (RACE CONDITION)

python多線程結束線程

This is the second and final part of the series. You can find the first part of the blog here. The first part covered basic python multi-threading. The second part discusses more advanced topics of multi-threading.

這是本系列的第二部分也是最後一部分。 您可以在此處找到部落格的第一部分。 第一部分介紹了基本的python多線程。 第二部分讨論多線程的更進階主題。

Python multi-threading is very similar to Dwight Schrute. If implemented properly it will be your good friend. If not, then we all know what happened in the snowball fight!

Python多線程與Dwight Schrute非常相似。 如果實施正确,它将是您的好朋友。 如果沒有,那麼我們都知道打雪仗發生了什麼!

僵局 (DEADLOCK)

Deadlock occurs when thread is waiting for a resource. Let’s take an example of the episode “Murder” from the office when Michael, Andy, Dwight and Pam find themselves in a fake Mexican handoff.

當線程正在等待資源時發生死鎖 。 讓我們以邁克爾·安迪,德懷特和帕姆發現自己在假墨西哥移交中的辦公室中的“ 謀殺案 ”為例。

python多線程結束線程_Python多線程和Office第2部分 僵局 (DEADLOCK) 背鎖 (DESERTED LOCK) 活鎖 (LIVELOCK) 信号 (SEMAPHORE) 比賽條件 (RACE CONDITION)

Let’s consider there are four guns (locks) and each person will need 2 guns (locks) to shoot the other person. They wait for 1000 sec before starting to shoot. Following code represents this

讓我們考慮有四把槍(鎖),每個人需要兩把槍(鎖)才能射擊另一個人。 他們等待1000秒才能開始拍攝。 以下代碼代表了這一點

import threading
import timei_will_shoot_you = 1000gun_a = threading.Lock()
gun_b = threading.Lock()
gun_c = threading.Lock()
gun_d = threading.Lock()def mexican_standoff(gun1, gun2):
    global i_will_shoot_you
    name = threading.current_thread().getName()
    while i_will_shoot_you > 0:
        gun1.acquire()
        gun2.acquire()
        i_will_shoot_you -= 1
        print(f"{name}: Warning I will kill you after {i_will_shoot_you} sec")
        time.sleep(0.1)
        gun1.release()
        gun2.release()if __name__ == '__main__':
    threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_a, gun2=gun_b), name='Michael').start()
    threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_b, gun2=gun_c), name='Andy').start()
    threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_c, gun2=gun_d), name='Pam').start()
    threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_d, gun2=gun_a), name='Dwight').start()
           

Sample output

樣品輸出

Michael: Warning I will kill you after 999 sec
Pam: Warning I will kill you after 998 sec
Michael: Warning I will kill you after 997 sec
Pam: Warning I will kill you after 996 sec
Michael: Warning I will kill you after 995 sec
Pam: Warning I will kill you after 994 sec
Michael: Warning I will kill you after 993 sec
Pam: Warning I will kill you after 992 sec
Michael: Warning I will kill you after 991 sec
Pam: Warning I will kill you after 990 sec
Pam: Warning I will kill you after 989 sec
Michael: Warning I will kill you after 988 sec
Pam: Warning I will kill you after 987 sec
Michael: Warning I will kill you after 986 sec
Michael: Warning I will kill you after 985 sec
Dwight: Warning I will kill you after 984 sec
Dwight: Warning I will kill you after 983 sec
Pam: Warning I will kill you after 982 sec
Andy: Warning I will kill you after 981 sec
Andy: Warning I will kill you after 980 sec
Michael: Warning I will kill you after 979 sec
Dwight: Warning I will kill you after 978 sec
Dwight: Warning I will kill you after 977 sec
           

We have four locks, gun_a, gun_b, gun_c and gun_d. Each thread tries to acquire 2 locks (guns) before executing print statement (critical section). This may lead to a deadlock condition. Let’s say Pam has picked up gun_a (lock1) but is waiting to pick up gun_b (lock2). But turns out Andy has picked up gun_b (lock1) and is waiting for a different gun (lock2). This leads to a deadlock. As each character (thread) is waiting for a different lock to be available, while holding onto one gun (lock).

我們有四個鎖,分别是gun_a,gun_b,gun_c和gun_d。 每個線程在執行列印語句(關鍵部分)之前嘗試擷取2個鎖(槍)。 這可能導緻死鎖。 假設Pam已拿起gun_a(lock1),但正在等待拿起gun_b(lock2)。 但是事實證明,安迪(Andy)拿起gun_b(lock1),正在等待另一把槍(lock2)。 這導緻死鎖。 當每個角色(線程)在等待另一把鎖可用時,同時握住一把槍(鎖)。

One way to avoid this is, if threads try to acquire a common lock first. This way all the threads try to acquire one common lock first before trying to acquire a different lock. Thus, avoiding deadlock. We can edit the code so that all threads first try to acquire gun_a lock.

避免這種情況的一種方法是,如果線程首先嘗試擷取公共鎖。 這樣,所有線程先嘗試擷取一個公共鎖,然後再嘗試擷取其他鎖。 是以,避免了死鎖。 我們可以編輯代碼,以便所有線程首先嘗試擷取gun_a鎖。

threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_a, gun2=gun_b), name='Michael').start()
    threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_a, gun2=gun_c), name='Andy').start()
    threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_a, gun2=gun_b), name='Pam').start()
    threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_a, gun2=gun_d), name='Dwight').start()
           

背鎖 (DESERTED LOCK)

When there are multiple threads competing to acquire a lock to execute critical section and due to some error thread that has acquired the lock crashes before releasing the lock. Because of this other threads wait indefinitely to acquire the lock.

當有多個線程競争擷取鎖以執行關鍵部分時,并且由于某些錯誤而導緻擷取鎖的線程崩潰,是以在釋放鎖之前會崩潰。 是以,其他線程會無限期等待以擷取鎖。

Let’s demonstrate this using the episode when Michael quits Dunder Mifflin and decides to moves to Colorado with Holly.

讓我們用邁克爾退出鄧德·米夫林(Dunder Mifflin)并決定與霍莉(Holly)搬到科羅拉多州時的一集來證明這一點。

import threadingleaving_show = threading.Lock()def leaving_the_show(character=None):
    # list of characters killed in the movie
    leaving_show.acquire()
    print(f"{character} has acquired the lock.")
    if character == "Michael":
        print(f"Sadly, {character} has decided to leave the show! :(")
        exit(1)
    leaving_show.release()
    print(f"{character} has released the lock.")if __name__ == '__main__':
    characters = ["Toby", "Dwight", "Angela",
                  "Jim", "Pam", "Michael", "Stanley", "Oscar", "Creed"]
    threads = list()# Defining different threads for each character
    for character in characters:
        threads.append(threading.Thread(
                                target=leaving_the_show,
                                kwargs=dict(character=character),
                                name=character)
                                )# This will start the threads
    for thread in threads:
        thread.start()# Waiting for all the threads to finish execution
    for thread in threads:
        thread.join()
           

Sample output

樣品輸出

Toby has acquired the lock.
Toby has released the lock.
Dwight has acquired the lock.
Dwight has released the lock.
Angela has acquired the lock.
Angela has released the lock.
Jim has acquired the lock.
Jim has released the lock.
Pam has acquired the lock.
Pam has released the lock.
Michael has acquired the lock.
Sadly, Michael has decided to leave the show! :(
           

In the code above, each of the thread acquire leaving_show lock to execute print (critical section) in function leaving_the_show. However, when thread Michael is executing it exits the program before releasing the lock. This leads to subsequent threads waiting infinitely to acquire the lock leaving_show.

在上面的代碼中,每個線程都擷取Leave_show鎖,以在函數Leave_the_show中執行列印(關鍵部分)。 但是,當線程Michael正在執行時,它在釋放鎖之前退出程式。 這導緻後續線程無限期地等待擷取鎖leaking_show。

To avoid this we can use try-finally for acquiring and releasing the lock.

為了避免這種情況,我們可以使用try-finally來擷取和釋放鎖。

def leaving_the_show(character=None):
    # list of characters killed in the movie
    try:
        leaving_show.acquire()
        print(f"{character} has acquired the lock.")
        if character == "Dwight":
            print ("I, Dwight K Schrute will make sure that the show must go on!")
        if character == "Michael":
            print(f"Sadly, {character} had decided to leave the show! :(")
            exit(1)
    finally:
        leaving_show.release()
    print(f"{character} has released the lock.")
           

Sample output

樣品輸出

Toby has acquired the lock.
Toby has released the lock.
Dwight has acquired the lock.
I, Dwight K Schrute will make sure that the show must go on!
Dwight has released the lock.
Angela has acquired the lock.
Angela has released the lock.
Jim has acquired the lock.
Jim has released the lock.
Pam has acquired the lock.
Pam has released the lock.
Michael has acquired the lock.
Sadly, Michael had decided to leave the show! :(
Stanley has acquired the lock.
Stanley has released the lock.
Oscar has acquired the lock.
Oscar has released the lock.
Creed has acquired the lock.
Creed has released the lock.
           

活鎖 (LIVELOCK)

Livelocks are similar to deadlocks. Livelocks occur when threads are designed to respond to actions of other threads (often to avoid deadlock), which leads to threads actively trying to resolve a conflict without making any progress.

活鎖類似于死鎖。 當線程被設計為響應其他線程的動作(通常是為了避免死鎖)時,就會發生動态鎖,這導緻線程主動嘗試解決沖突而不取得任何進展。

To demonstrate livelock, let’s recall the Mexican standoff example that we used for deadlock. Each character will try to pick up gun1 first, when trying to pickup second gun it will check if gun2 is available to be picked up, if not gun1 will be politely dropped.

為了示範活鎖,讓我們回想一下用于死鎖的墨西哥僵持示例。 每個角色将首先嘗試拾取gun1,在嘗試拾取第二把槍時,它将檢查gun2是否可以被拾取,如果不是,将禮貌地丢棄gun1。

import threadingi_will_shoot_you = 1000gun_a = threading.Lock()
gun_b = threading.Lock()
gun_c = threading.Lock()def mexican_standoff(gun1, gun2):
    global i_will_shoot_you
    name = name = threading.current_thread().getName()
    while i_will_shoot_you > 0:
        gun1.acquire()
        if not gun2.acquire(blocking=False):
            print(f"{name} politely dropped the gun to avoid deadlock")
            gun1.release()
        else:
            i_will_shoot_you -= 1
            print(f"Warning I will kill you after {i_will_shoot_you} sec")
            gun1.release()
            gun2.release()if __name__ == '__main__':
    threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_a, gun2=gun_b), name='Michael').start()
    threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_b, gun2=gun_c), name='Andy').start()
    threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_c, gun2=gun_b), name='Pam').start()
    threading.Thread(target=mexican_standoff, kwargs=dict(gun1=gun_b, gun2=gun_c), name='Dwight').start()
           

Sample output

樣品輸出

Warning I will kill you after 977 sec
Warning I will kill you after 976 sec
Warning I will kill you after 975 sec
Warning I will kill you after 974 sec
Pam politely dropped the gun to avoid deadlock
Pam politely dropped the gun to avoid deadlock
Dwight politely dropped the gun to avoid deadlock
Pam politely dropped the gun to avoid deadlock
Michael politely dropped the gun to avoid deadlock
Michael politely dropped the gun to avoid deadlock
Michael politely dropped the gun to avoid deadlock
Andy politely dropped the gun to avoid deadlock
Pam politely dropped the gun to avoid deadlock
Pam politely dropped the gun to avoid deadlock
Andy politely dropped the gun to avoid deadlock
Pam politely dropped the gun to avoid deadlock
Andy politely dropped the gun to avoid deadlock
Michael politely dropped the gun to avoid deadlock
Andy politely dropped the gun to avoid deadlock
Pam politely dropped the gun to avoid deadlock
Michael politely dropped the gun to avoid deadlock
Michael politely dropped the gun to avoid deadlock
Michael politely dropped the gun to avoid deadlock
Pam politely dropped the gun to avoid deadlock
Pam politely dropped the gun to avoid deadlock
Michael politely dropped the gun to avoid deadlock
Michael politely dropped the gun to avoid deadlock
Pam politely dropped the gun to avoid deadlock
           

As seen in the output of the code, threads try to acquire both the locks (gun1 and gun2), if gun2 is not available then gun1 lock is released.This leads to livelock situation.

從代碼輸出中可以看到,線程嘗試同時擷取兩個鎖(gun1和gun2),如果gun2不可用,則gun1鎖被釋放,這導緻了活鎖情況。

Both deadlock and livelock cause program execution to not proceed. But there is a slight difference in behavior. In case of deadlocks threads are waiting for the resources, so CPU cycles are not consumed, while in livelock threads are actively trying to resolve the conflict leading to higher CPU usage. This is called ‘busy waiting’. CPU utilization is a good way to figure out if it’s a deadlock or a livelock.

死鎖和活動鎖都會導緻程式無法繼續執行。 但是行為上略有不同。 如果出現死鎖,線程正在等待資源,是以不會占用CPU周期,而在活鎖中,線程正在積極嘗試解決導緻更高CPU使用率的沖突。 這稱為“忙等待”。 CPU使用率是弄清楚是死鎖還是活鎖的好方法。

To avoid livelock, we can let the thread sleep for a random amount of time before trying to reacquire locks.

為避免發生活鎖,我們可以讓線程在嘗試重新擷取鎖之前随機Hibernate一段時間。

Let’s modify the mexican_standoff function with the change.

讓我們用更改來修改mexican_standoff函數。

def mexican_standoff(gun1, gun2):
    global i_will_shoot_you
    name = name = threading.current_thread().getName()
    while i_will_shoot_you > 0:
        gun1.acquire()
        if not gun2.acquire(blocking=False):
            print(f"{name} politely dropped the gun to avoid deadlock")
            gun1.release()
            time.sleep(random()/100)
        else:
            i_will_shoot_you -= 1
            print(f"Warning I will kill you after {i_will_shoot_you} sec")
            gun1.release()
            gun2.release()
           

In the function above threads sleep for a random time after dropping gun1 (releasing lock) and trying to reacquire the lock.

在上面的函數中,在釋放gun1(釋放鎖)并嘗試重新擷取鎖之後,線程會随機睡眠一段時間。

信号 (SEMAPHORE)

Semaphore is much like mutex, it protects the critical section of the program, but it also provides additional functionalities. Semaphore allows multiple threads to access the critical section at a time. It keeps a counter to track number of threads accessing the critical section. As long as counter is more than zero threads can acquire the semaphore and decrement the counter. When counter hits zero, threads wait in a queue till semaphore is available again. Another difference between semaphore and mutex is, in case of semaphore, it can be acquired or released by different threads, while mutex can be acquired or released by same thread.

信号量與互斥量非常相似,它可以保護程式的關鍵部分,但還提供其他功能。 信号量允許多個線程一次通路關鍵部分。 它保留一個計數器來跟蹤通路關鍵部分的線程數。 隻要計數器大于零,線程就可以擷取信号量并使計數器遞減。 當計數器為零時,線程将在隊列中等待,直到信号燈再次可用。 信号量和互斥量之間的另一個差別是,在信号量的情況下,它可以由不同的線程擷取或釋放,而互斥量可以由同一線程擷取或釋放。

To demonstrate this let’s take example of Darryl leaving Dunder Mifflin and deciding to dance with his colleagues. In this case Darryl is the critical section and we will use semaphore to allow 2 threads (characters) to dance with Darryl (accessing critical section).

為了說明這一點,讓我們以達裡爾離開鄧德·米夫林并決定與他的同僚共舞為例。 在這種情況下,Darryl是關鍵部分,我們将使用信号量允許2個線程(字元)與Darryl跳舞(通路關鍵部分)。

import threading
import timedancing = threading.Semaphore(2)def darryl_dancing():
    name = threading.current_thread().getName()
    with dancing:
        print(f"Darryl is dancing with {name}")
        time.sleep(1)if __name__ == '__main__':
    characters = ["Toby", "Creed", "Erin", "Kevin", "Nellie", "Oscar", "Clark"]
    threads = list()
    for character in characters:
        threads.append(threading.Thread(target=darryl_dancing, name=character))for thread in threads:
        thread.start()for thread in threads:
        thread.join()
           

Sample output

樣品輸出

Darryl is dancing with Toby
Darryl is dancing with Creed
Darryl is dancing with Erin
Darryl is dancing with Kevin
Darryl is dancing with Nellie
Darryl is dancing with Oscar
Darryl is dancing with Clark
           

In the code above we use dancing as semaphore object, which allows 2 threads to access the critical section at a time. While 2 threads are accessing the critical section, other threads will wait for current threads to release the semaphore. Also, if you notice I have used “with” keyword to acquire and release the semaphore in the code above. If we change semaphore definition to allow 1 thread at a time, then it becomes a binary semaphore and behaves just like mutex.

在上面的代碼中,我們将舞動用作信号量對象,該對象允許2個線程一次通路關鍵部分。 當2個線程正在通路關鍵部分時,其他線程将等待目前線程釋放信号量。 另外,如果您注意到我在上面的代碼中使用了“ with”關鍵字來擷取和釋放信号量。 如果我們将信号量定義更改為一次允許1個線程,那麼它将變為二進制信号量,其行為類似于互斥鎖。

比賽條件 (RACE CONDITION)

Let’s talk about real devil in multi-threading. When multiple threads execute critical section, due to scheduling of threads by the operating system it may result in different results.

讓我們談談多線程中的真正魔鬼。 當多個線程執行關鍵部分時,由于作業系統對線程的排程,可能會導緻不同的結果。

Let’s take 2 functions kevin_multiplication which will multiply accounts variable by 3 and oscar_addition which will add 10 to the accounts variable. Function “calculating” simulates threads spending time in calculations. As expected, thread Kevin takes 5 units to complete calculation and thread Oscar take 3 units.

讓我們使用2個函數kevin_multiplication,它将accounts變量乘以3,将oscar_addition乘以10的accounts變量。 函數“計算”模拟線程在計算中花費的時間。 正如預期的那樣,線程Kevin需要5個機關才能完成計算,線程Oscar需要3個機關。

import threadingaccounts = 1
doomsday = threading.Lock()def calculating(checks):
    task = 0
    for i in range(checks * 100000):
        task += 1def kevin_multiplication():
    global accounts
    calculating(5)
    with doomsday:
        print("Kevin is editing the accounts")
        accounts *= 3def oscar_addition():
    global accounts
    calculating(3)
    with doomsday:
        print("Oscar is editing the accounts")
        accounts += 10if __name__ == '__main__':
    threads = list()
    for i in range(10):
        threads.append(threading.Thread(target=kevin_multiplication, name="Kevin"))
        threads.append(threading.Thread(target=oscar_addition, name="Oscar"))for thread in threads:
        thread.start()for thread in threads:
        thread.join()print(f"Final account tally is: {accounts}")
           

Sample output

樣品輸出

First run:
Oscar is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Final account tally is: 926689Second run:
Kevin is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Final account tally is: 399709
           

In the sample output above, in first run final account tally was 926689 while in the second run tally was 399709. This is due to the scheduling of the threads. If Dwight had setup doomsday device it would have beeped at least once already!

在上面的示例輸出中,第一次運作時最終帳戶的理算結果為926689,而第二次運作時最終的理算結果為399709。這是由于線程的排程所緻。 如果德懷特(Dwight)設定了世界末日裝置,它将至少已經發出一次哔聲!

To fix this we have to change how threads are modifying accounts variable regardless of the scheduling of the threads. We can use barrier in python threading module for this. Thread Kevin will modify the variable first and when it has modified the variable it will notify thread Oscar to start updating the variable. This way we will always get a consistent value.

為了解決這個問題,我們必須更改線程修改帳戶變量的方式,而與線程的排程無關。 我們可以在python線程子產品中使用barrier 。 線程Kevin将首先修改變量,并在修改變量後通知線程Oscar開始更新變量。 這樣,我們将始終獲得一緻的價值。

import threadingaccounts = 1
doomsday = threading.Lock()
beat_doomsday_device = threading.Barrier(20)def calculating(checks):
    task = 0
    for i in range(checks * 100000):
        task += 1def kevin_multiplication():
    global accounts
    calculating(5)
    with doomsday:
        print("Kevin is editing the accounts")
        accounts *= 3
    beat_doomsday_device.wait()def oscar_addition():
    global accounts
    calculating(3)
    beat_doomsday_device.wait()
    with doomsday:
        print("Oscar is editing the accounts")
        accounts += 10if __name__ == '__main__':
    threads = list()
    for i in range(10):
        threads.append(threading.Thread(target=kevin_multiplication, name="Kevin"))
        threads.append(threading.Thread(target=oscar_addition, name="Oscar"))for thread in threads:
        thread.start()for thread in threads:
        thread.join()print(f"Final account tally is: {accounts}")
           

Sample output

樣品輸出

First run:
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Final account tally is: 59149Second run:
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Kevin is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Oscar is editing the accounts
Final account tally is: 59149
           

In the code sample above, we have created a barrier beat_doomsday_device. Thread Kevin complete execution and waits at barrier. While thread Oscar waits for thread Kevin to finish execution before changing value of variable accounts.

在上面的代碼示例中,我們建立了一個屏障beat_doomsday_device。 線程Kevin完成執行并等待障礙。 線上程Oscar等待線程Kevin完成執行之前,更改變量帳戶的值。

This brings us to the end of the blog! This was a great learning experience for me and I hope it was useful to you too. I leave you with my favorite the Office quote.

這将我們帶到部落格的結尾! 這對我來說是一次很棒的學習經曆,我希望它對您也有用。 我給我留下我最喜歡的Office報價。

python多線程結束線程_Python多線程和Office第2部分 僵局 (DEADLOCK) 背鎖 (DESERTED LOCK) 活鎖 (LIVELOCK) 信号 (SEMAPHORE) 比賽條件 (RACE CONDITION)

Disclaimer: Sample output of code snippets might vary depending on the operating system configurations.

免責聲明:代碼段的樣本輸出可能會因作業系統配置而異。

翻譯自: https://medium.com/swlh/python-multi-threading-and-the-office-part-2-e4a10e4e0afe

python多線程結束線程