Python 線程 5 分鐘完全解讀

線程,有時被稱爲輕量進程,是程序執行流的最小單元。一個標準的線程由線程 ID,當前指令指針 (PC),寄存器集合和堆棧組成。線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程不擁有私有的系統資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創建和撤消另一個線程,同一進程中的多個線程之間可以併發執行。

現代處理器都是多核的,幾核處理器只能同時處理幾個線程,多線程執行程序看起來是同時進行,實際上是 CPU 在多個線程之間快速切換執行,這中間就涉及到上下問切換,所謂的上下文切換就是指一個線程 Thread 被分配的時間片用完了之後,線程的信息被保存起來,CPU 執行另外的線程,再到 CPU 讀取線程 Thread 的信息並繼續執行 Thread 的過程。

線程模塊

Python 創建 Thread 對象語法如下:

import threading
threading.Thread(target=None, name=None,  args=())

主要參數說明:

Python 中實現多線程有兩種方式:函數式創建線程和創建線程類。

第一種創建線程方式:

創建線程的時候,只需要傳入一個執行函數和函數的參數即可完成 threading.Thread 實例的創建。下面的例子使用 Thread 類來產生 2 個子線程,然後啓動 2 個子線程並等待其結束:

import threading
import time,random,math
# idx 循環次數
def printNum(idx):
for num in range(idx ):
#打印當前運行的線程名字
print("{0}\tnum={1}".format(threading.current_thread().getName(), num))
        delay = math.ceil(random.random()*2)
        time.sleep(delay)
if __name__ =='__main__':
    th1 = threading.Thread(target=printNum, args=(2,),)
    th2 = threading.Thread(target=printNum, args=(3,),)
#啓動2個線程
th1.start()
    th2.start()
#等待至線程中止
    th1.join()
    th2.join()
print("{0} 線程結束".format(threading.current_thread().getName()))

運行腳本得到以下結果:

運行腳本默認會啓動一個線程,把該線程稱爲主線程,主線程有可以啓動新的線程,Python 的 threading 模塊有個 current_thread() 函數,它將返回當前線程的示例。從當前線程的示例可以獲得前運行線程名字,核心代碼如下:

threading.current_thread().getName()

啓動一個線程就是把一個函數和參數傳入並創建 Thread 實例,然後調用 start() 開始執行:

th1 = threading.Thread(target=printNum, args=(2,),)
th1.start()

從返回結果可以看出主線程示例的名字叫 MainThread,子線程的名字在創建時指定, 本例創建了 2 個子線程,名字叫 thread1 和 thread2。如果沒有給線程起名字,Python 就自動給線程命名爲 Thread-1,Thread-2… 等等。在本例中定義了線程函數 printNum(), 打印 idx 次記錄後退出,每次打印使用 time.sleep() 讓程序休眠一段時間。

第二種創建線程方式:創建線程類

直接創建 threading.Thread 的子類來創建一個線程對象, 實現多線程。通過繼承 Thread 類,並重寫 Thread 類的 run() 方法,在 run() 方法中定義具體要執行的任務。在 Thread 類中,提供了一個 start() 方法用於啓動新進程,線程啓動後會自動調用 run() 方法:

import threading
import time,random,math
classMutliThread(threading.Thread):
def __init__(self, threadName,num):
        threading.Thread.__init__(self)
self.name = threadName
self.num = num
def run(self):
for i in range(self.num):
print("{0} i={1}".format(threading.current_thread().getName(), i))
            delay = math.ceil(random.random()*2)
            time.sleep(delay)
if __name__ =='__main__':
    thr1 =MutliThread("thread1",3)
    thr2 =MutliThread("thread2",2)
# 啓動線程
    thr1.start()
    thr2.start()
# 等待至線程中止
    thr1.join()
    thr2.join()
print("{0} 線程結束".format(threading.current_thread().getName()))

運行腳本得到以下結果:

thread1 i=0
thread2 i=0
thread1 i=1
thread2 i=1
thread1 i=2
MainThread線程結束

從返回結果可以看出,通過創建 Thread 類來產生 2 個線程對象 thr1 和 thr2,重寫 Thread 類的 run() 函數,把業務邏輯放入其中,通過調用線程對象的 start() 方法啓動線程。通過調用線程對象的 join() 函數,等待該線程完成,在繼續下面的操作。

在本例中,主線程 MainThread 等待子線程 thread1 和 thread2 線程運行結束後才輸出” MainThread 線程結束”。如果子線程 thread1 和 thread2 不調用 join() 函數,那麼主線程 MainThread 和 2 個子線程是並行執行任務的,2 個子線程加上 join() 函數後,程序就變成順序執行了。所以子線程用到 join() 的時候,通常都是主線程等到其他多個子線程執行完畢後再繼續執行,其他的多個子線程並不需要互相等待。

守護線程

在線程模塊中,使用子線程對象用到 join() 函數,主線程需要依賴子線程執行完畢後才繼續執行代碼。如果子線程不使用 join() 函數,主線程和子線程是並行運行的,沒有依賴關係,主線程執行了,子線程也在執行。

在多線程開發中,如果子線程設定爲了守護線程,守護線程會等待主線程運行完畢後被銷燬。一個主線程可以設置多個守護線程,守護線程運行的前提是,主線程必須存在,如果主線程不存在了,守護線程會被銷燬。

在本例中創建 1 個主線程 3 個子線程,讓主線程和子線程並行執行。內容如下:

import threading, time
def run(taskName):
print("任務:", taskName)
    time.sleep(2)
print("{0} 任務執行完畢".format(taskName))# 查看每個子線程
if __name__ =='__main__':
    start_time = time.time()
for i in range(3):
        thr = threading.Thread(target=run, args=("task-{0}".format(i),))
# 把子線程設置爲守護線程
        thr.setDaemon(True)
        thr.start()
# 查看主線程和當前活動的所有線程數
print("{0}線程結束,當線程數量={1}".format( threading.current_thread().getName(), threading.active_count()))
print("消耗時間:", time.time()- start_time)

運行腳本得到以下結果:

任務: task-0
任務: task-1
任務: task-2
MainThread線程結束,當線程數量=4
消耗時間:0.0009751319885253906
task-2任務執行完畢
task-0任務執行完畢
task-1任務執行完畢

從返回結果可以看出,當前的線程個數是 4,線程個數 = 主線程數 + 子線程數,在本例中有 1 個主線程和 3 個子線程。主線程執行完畢後,等待子線程執行完畢,程序纔會退出。

在本例的基礎上,把所有的子線程都設置爲守護線程。子線程變成守護線程後,只要主線程執行完畢,程序不管子線程有沒有執行完畢,程序都會退出。使用線程對象的 setDaemon(True) 函數來設置守護線程:

import threading, time
def run(taskName):
print("任務:", taskName)
    time.sleep(2)
print("{0} 任務執行完畢".format(taskName))
if __name__ =='__main__':
    start_time = time.time()
for i in range(3):
        thr = threading.Thread(target=run, args=("task-{0}".format(i),))
# 把子線程設置爲守護線程,在啓動線程前設置
thr.setDaemon(True)
        thr.start()
# 查看主線程和當前活動的所有線程數
    thrName = threading.current_thread().getName()
    thrCount = threading.active_count()
print("{0}線程結束,當線程數量={1}".format(thrName, thrCount))
print("消耗時間:", time.time()- start_time)

運行腳本得到以下結果:

任務: task-0
任務: task-1
任務: task-2
MainThread線程結束,當線程數量=4
消耗時間:0.0010023117065429688

從本例的返回結果可以看出,主線程執行完畢後,程序不會等待守護線程執行完畢後就退出了。設置線程對象爲守護線程,一定要在線程對象調用 start() 函數前設置。

多線程的鎖機制

多線程編程訪問共享變量時會出現問題,但是多進程編程訪問共享變量不會出現問題。因爲多進程中,同一個變量各自有一份拷貝存在於每個進程中,互不影響,而多線程中,所有變量都由所有線程共享。

多個進程之間對內存中的變量不會產生衝突,一個進程由多個線程組成,多線程對內存中的變量進行共享時會產生影響,所以就產生了死鎖問題,怎麼解決死鎖問題是本節主要介紹的內容。

1、變量的作用域

一般在函數體外定義的變量稱爲全局變量,在函數內部定義的變量稱爲局部變量。全局變量所有作用域都可讀,局部變量只能在本函數可讀。函數在讀取變量時,優先讀取函數本身自有的局部變量,再去讀全局變量。 
內容如下:

# 全局變量
balance =1
def change():
# 定義全局變量
global balance
    balance =100
# 定義局部變量
    num =20
print("change() balance={0}".format(balance))
if __name__ =="__main__":
    change()
print("修改後的 balance={0}".format(balance))

運行腳本得到以下結果:

change() balance=100
修改後的 balance=100

如果註釋掉 change() 函數里的 global:

v1,那麼得到的返回值是。
change() balance=100
修改後的 balance=1

在本例中在 change() 函數外定義的變量 balance 是全局變量,在 change() 函數內定義的變量 num 是局部變量,全局變量默認是可讀的,可以在任何函數中使用,如果需要改變全局變量的值,需要在函數內部使用 global 定義全局變量,本例中在 change() 函數內部使用 global 定義全局變量 balance, 在函數里就可以改變全局變量了。

在函數里可以使用全局變量,但是在函數里不能改變全局變量。想實現多個線程共享變量,需要使用全局變量。在方法里加上全局關鍵字 global 定義全局變量,多線程纔可以修改全局變量來共享變量。

2、多線程中的鎖

多線程同時修改全局變量時會出現數據安全問題,線程不安全就是不提供數據訪問保護,有可能出現多個線程先後更改數據造成所得到的數據是髒數據。在本例中我們生成 2 個線程同時修改 change() 函數里的全局變量 balance 時,會出現數據不一致問題。

本案例文件名爲 PythonFullStack\Chapter03\threadDemo03.py,內容如下:

import threading
balance =100
def change(num, counter):
global balance
for i in range(counter):
        balance += num
        balance -= num
if balance !=100:
# 如果輸出這句話,說明線程不安全
print("balance=%d"% balance)
break
if __name__ =="__main__":
    thr1 = threading.Thread(target=change,args=(100,500000),name='t1')
    thr2 = threading.Thread(target=change,args=(100,500000),name='t2')
    thr1.start()
    thr2.start()
    thr1.join()
    thr2.join()
print("{0} 線程結束".format(threading.current_thread().getName()))

運行以上腳本,當 2 個線程運行次數達到 500000 次時,會出現以下結果:

balance=200
MainThread線程結束

在本例中定義了一個全局變量 balance, 初始值爲 100,當啓動 2 個線程後,先加後減,理論上 balance 應該爲 100。線程的調度是由操作系統決定的,當線程 t1 和 t2 交替執行時,只要循環次數足夠多,balance 結果就不一定是 100 了。從結果可以看出,在本例中線程 t1 和 t2 同時修改全局變量 balance 時,會出現數據不一致問題。

注意

在多線程情況下,所有的全局變量有所有線程共享。所以,任何一個變量都可以被任何一個線程修改,因此,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。

互斥鎖的核心代碼如下:

聲明:

本文於網絡整理,版權歸原作者所有,如來源信息有誤或侵犯權益,請聯繫我們刪除或授權事宜。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MNq1lKbaOEUXVFLck7DcPw