threading 模塊
在 Python 中實現(xiàn)多線程奇徒,可以利用 threading
模塊中的 Thread
來實現(xiàn)尉间,其用法 mutliprocessing
模塊中的 Process
類偿乖。
th = threading.Thread([ target ],[ args ],[ kwargs ])
其中 target
參數(shù)是線程的目標(biāo)函數(shù),args
和 kwargs
是目標(biāo)函數(shù)的參數(shù)哲嘲。如果不指定 target
參數(shù)贪薪,將會默認(rèn)調(diào)用線程對象中的 run
方法。
看一個例子:
from threading import Thread
from time import sleep
# 線程的執(zhí)行函數(shù)
def handlePrint(startRange):
while True:
startRange += 1
print("我從 %d 開始輸出眠副,現(xiàn)在的輸出結(jié)果是 %d"%(startRange-1,startRange))
sleep(0.5)
# 在主模塊中開啟線程
if __name__ == "__main__":
for i in range(5):
th = Thread(target = handlePrint,args = (i,))
th.start()
運(yùn)行結(jié)果如下:
PS C:\Users\Charley\Desktop\py> python .\py.py
我從 0 開始輸出画切,現(xiàn)在的輸出結(jié)果是 1
我從 1 開始輸出,現(xiàn)在的輸出結(jié)果是 2
我從 2 開始輸出囱怕,現(xiàn)在的輸出結(jié)果是 3
我從 3 開始輸出霍弹,現(xiàn)在的輸出結(jié)果是 4
我從 4 開始輸出,現(xiàn)在的輸出結(jié)果是 5
我從 5 開始輸出娃弓,現(xiàn)在的輸出結(jié)果是 6
我從 3 開始輸出典格,現(xiàn)在的輸出結(jié)果是 4
我從 4 開始輸出,現(xiàn)在的輸出結(jié)果是 5
我從 2 開始輸出台丛,現(xiàn)在的輸出結(jié)果是 3
我從 1 開始輸出耍缴,現(xiàn)在的輸出結(jié)果是 2
我從 3 開始輸出,現(xiàn)在的輸出結(jié)果是 4
我從 2 開始輸出挽霉,現(xiàn)在的輸出結(jié)果是 3
我從 4 開始輸出防嗡,現(xiàn)在的輸出結(jié)果是 5
...
借助 threading
模塊下的 current_thread
函數(shù),可以獲取當(dāng)前的線程名:
from threading import Thread
from time import sleep
# 線程的執(zhí)行函數(shù)
def handlePrint(startRange):
from threading import current_thread
name = current_thread().name
while True:
startRange += 1
print("我是 %s侠坎,現(xiàn)在的輸出結(jié)果是 %d"%(name,startRange))
sleep(0.5)
# 在主模塊中開啟線程
if __name__ == "__main__":
for i in range(5):
th = Thread(target = handlePrint,args = (i,))
th.start()
運(yùn)行結(jié)果為:
PS C:\Users\Charley\Desktop\py> python .\py.py
我是 Thread-1蚁趁,現(xiàn)在的輸出結(jié)果是 1
我是 Thread-2,現(xiàn)在的輸出結(jié)果是 2
我是 Thread-3硅蹦,現(xiàn)在的輸出結(jié)果是 3
我是 Thread-4荣德,現(xiàn)在的輸出結(jié)果是 4
我是 Thread-5闷煤,現(xiàn)在的輸出結(jié)果是 5
我是 Thread-2童芹,現(xiàn)在的輸出結(jié)果是 3
我是 Thread-1涮瞻,現(xiàn)在的輸出結(jié)果是 2
我是 Thread-3,現(xiàn)在的輸出結(jié)果是 4
我是 Thread-4假褪,現(xiàn)在的輸出結(jié)果是 5
我是 Thread-5署咽,現(xiàn)在的輸出結(jié)果是 6
我是 Thread-1,現(xiàn)在的輸出結(jié)果是 3
我是 Thread-4生音,現(xiàn)在的輸出結(jié)果是 6
我是 Thread-2宁否,現(xiàn)在的輸出結(jié)果是 4
擴(kuò)展線程類
我們也可以對線程類進(jìn)行擴(kuò)展,以實現(xiàn)一個獨立的模塊:
from threading import Thread
from time import sleep
# 擴(kuò)展線程類
class MyThread(Thread):
def __init__(self,startRange):
Thread.__init__(self)
# 處理新建對象時傳入的參數(shù)
self.__startRange = startRange
# 聲明 run 方法
def run(self):
while True:
print("我是 %s缀遍,我正在輸出 %d"%(self.name,self.__startRange))
self.__startRange += 1
sleep(1)
# 主模塊中開啟線程
if __name__ == '__main__':
for i in range(5):
th = MyThread(i)
th.start()
運(yùn)行結(jié)果:
PS C:\Users\Charley\Desktop\py> python .\py.py
我是 Thread-1慕匠,我正在輸出 0
我是 Thread-2,我正在輸出 1
我是 Thread-3域醇,我正在輸出 2
我是 Thread-4台谊,我正在輸出 3
我是 Thread-5,我正在輸出 4
我是 Thread-1譬挚,我正在輸出 1
我是 Thread-2锅铅,我正在輸出 2
我是 Thread-4,我正在輸出 4
我是 Thread-5减宣,我正在輸出 5
我是 Thread-3盐须,我正在輸出 3
我是 Thread-2,我正在輸出 3
我是 Thread-1漆腌,我正在輸出 2
我是 Thread-4贼邓,我正在輸出 5
我是 Thread-3,我正在輸出 4
我是 Thread-5闷尿,我正在輸出 6
多個線程使用一份全局變量
有時候我們會在多個線程中操縱一個全局變量塑径,以提高性能(比如大數(shù)字累加),這樣會產(chǎn)生什么問題呢悠砚?我們可以看下面的代碼:
from threading import Thread
num = 0
# 讓 counter1 函數(shù)對 num 累加 100 萬次
def counter1():
global num
for i in range(1000000):
num += 1
print("counter1 計算出來的 num 的值是 %d"%num)
# 讓 counter2 函數(shù)對 num 累加 100 萬次
def counter2():
global num
for i in range(1000000):
num += 1
print("counter2 計算出來的 num 的值是 %d"%num)
# 主模塊中開啟線程
if __name__ == '__main__':
th1 = Thread(target = counter1)
th2 = Thread(target = counter2)
th1.start()
th2.start()
th1.join()
th2.join()
print("主線程中獲取的 num 的值是 %d"%num)
運(yùn)行結(jié)果(每次運(yùn)行的結(jié)果可能都不一樣):
PS C:\Users\Charley\Desktop\py> python .\py.py
counter1 計算出來的 num 的值是 1082089
counter2 計算出來的 num 的值是 1183834
主線程中獲取的 num 的值是 1183834
PS C:\Users\Charley\Desktop\py>
這里并不是我們期望的 200 萬晓勇,為什么呢?
因為計算操作是由操作系統(tǒng)進(jìn)行調(diào)度的灌旧,操作系統(tǒng)并不會在等待一個函數(shù)甚至語句執(zhí)行完成后再調(diào)用后面的語句和函數(shù)绑咱,整個過程是不定的。有可能在 counter1
中對線程進(jìn)行了累加運(yùn)算枢泰,但并沒有賦值(只進(jìn)行了 num = num + 1 的后半部分就將控制權(quán)交給 counter2
函數(shù)了)描融,counter2
函數(shù)獲取控制權(quán)后,也進(jìn)行累加操作衡蚂,我們假定這次他進(jìn)行了一次完整的累加操作窿克。此后操作系統(tǒng)繼續(xù)進(jìn)行調(diào)度骏庸,執(zhí)行 counter1
未被執(zhí)行完成的賦值操作(等號左邊部分),該操作對 counter2
的上一次執(zhí)行結(jié)果進(jìn)行了覆蓋年叮,因此整個結(jié)果都是不穩(wěn)定的具被。
我們知道了,計算不穩(wěn)定的原因是多個線程相互搶占資源造成的只损。為了解決這個問題一姿,我們可以有以下的一些解決方案。
沖突線程延期執(zhí)行
我們可以讓沖突線程在前一個線程執(zhí)行之后再執(zhí)行跃惫,這個方案很簡單叮叹,只需稍微改變下調(diào)用 join
方法的位置:
from threading import Thread
num = 0
# 讓 counter1 函數(shù)對 num 累加 100 萬次
def counter1():
global num
for i in range(1000000):
num += 1
print("counter1 計算出來的 num 的值是 %d"%num)
# 讓 counter2 函數(shù)對 num 累加 100 萬次
def counter2():
global num
for i in range(1000000):
num += 1
print("counter2 計算出來的 num 的值是 %d"%num)
# 主模塊中開啟線程
if __name__ == '__main__':
th1 = Thread(target = counter1)
th2 = Thread(target = counter2)
th1.start()
th1.join()
th2.start()
th2.join()
print("主線程中獲取的 num 的值是 %d"%num)
運(yùn)行結(jié)果:
PS C:\Users\Charley\Desktop\py> python .\py.py
counter1 計算出來的 num 的值是 1000000
counter2 計算出來的 num 的值是 2000000
主線程中獲取的 num 的值是 2000000
PS C:\Users\Charley\Desktop\py>
使用標(biāo)識變量區(qū)分不同線程
也可以使用標(biāo)識變量進(jìn)行區(qū)分:
from threading import Thread
num = 0
flag = 1
# 讓 counter1 函數(shù)對 num 累加 100 萬次
def counter1():
global num,flag
for i in range(1000000):
num += 1
flag = 0
print("counter1 計算出來的 num 的值是 %d"%num)
# 讓 counter2 函數(shù)對 num 累加 100 萬次
def counter2():
global num
# counter2 線程一直輪詢等待 counter1 改變標(biāo)識變量
while True:
if not flag:
for i in range(1000000):
num += 1
break
print("counter2 計算出來的 num 的值是 %d"%num)
# 主模塊中開啟線程
if __name__ == '__main__':
th1 = Thread(target = counter1)
th2 = Thread(target = counter2)
th1.start()
th2.start()
th1.join()
th2.join()
print("主線程中獲取的 num 的值是 %d"%num)
運(yùn)行結(jié)果:
PS C:\Users\Charley\Desktop\py> python .\py.py
counter1 計算出來的 num 的值是 1000000
counter2 計算出來的 num 的值是 2000000
主線程中獲取的 num 的值是 2000000
PS C:\Users\Charley\Desktop\py>
使用回調(diào)函數(shù)
也可以可以將回調(diào)函數(shù)作為參數(shù)傳入 counter1
函數(shù),counter1
累加完成后執(zhí)行:
from threading import Thread
num = 0
# 讓 counter1 函數(shù)對 num 累加 100 萬次
def counter1(callback):
global num
for i in range(1000000):
num += 1
print("counter1 計算出來的 num 的值是 %d"%num)
callback()
# 讓 counter2 函數(shù)對 num 累加 100 萬次
def counter2():
global num
for i in range(1000000):
num += 1
print("counter2 計算出來的 num 的值是 %d"%num)
# 回調(diào)函數(shù)爆存,用來在 counter1 執(zhí)行完成后執(zhí)行
def callback():
th2 = Thread(target = counter2)
th2.start()
th2.join()
# 主模塊中開啟線程
if __name__ == '__main__':
th1 = Thread(target = counter1, args = (callback,))
th1.start()
th1.join()
print("主線程中獲取的 num 的值是 %d"%num)
運(yùn)行結(jié)果:
PS C:\Users\Charley\Desktop\py> python .\py.py
counter1 計算出來的 num 的值是 1000000
counter2 計算出來的 num 的值是 2000000
主線程中獲取的 num 的值是 2000000
PS C:\Users\Charley\Desktop\py>
互斥鎖
針對多個線程搶占資源的問題蛉顽,上面給出了幾個比較“偏門”的解決方案,但都不是那么的“Python 范兒”先较,我們最好使用互斥鎖來解決這個問題携冤。
簡單理解,互斥鎖就是同一時間只能有一個線程占用資源拇泣,其他線程只有在該線程釋放掉資源后才能進(jìn)行操作噪叙,而在其他線程進(jìn)行操作時,也應(yīng)該首先對資源上鎖霉翔,這樣就不會因為相互搶占資源而造成不確定的情況了睁蕾。
使用互斥鎖,需要使用 threading
中的互斥鎖工具類 Lock
债朵。有了互斥鎖類子眶,我們只需關(guān)心在合理的位置進(jìn)行上鎖和解鎖就可以了:
from threading import Thread,Lock
num = 0
# 讓 counter1 函數(shù)對 num 累加 100 萬次
def counter1():
global num
# 上鎖
mutex.acquire()
for i in range(1000000):
num += 1
# 解鎖
mutex.release()
print("counter1 計算出來的 num 的值是 %d"%num)
# 讓 counter2 函數(shù)對 num 累加 100 萬次
def counter2():
global num
# 上鎖
mutex.acquire()
for i in range(1000000):
num += 1
# 解鎖
mutex.release()
print("counter2 計算出來的 num 的值是 %d"%num)
# 創(chuàng)建一個互斥鎖對象
mutex = Lock()
# 主模塊中開啟線程
if __name__ == '__main__':
th1 = Thread(target = counter1)
th2 = Thread(target = counter2)
th1.start()
th2.start()
th1.join()
th2.join()
print("主線程中獲取的 num 的值是 %d"%num)
運(yùn)行結(jié)果:
PS C:\Users\Charley\Desktop\py> python .\py.py
counter1 計算出來的 num 的值是 1000000
counter2 計算出來的 num 的值是 2000000
主線程中獲取的 num 的值是 2000000
PS C:\Users\Charley\Desktop\py>
local
上面主要解決了多線程之間相互搶占資源的問題,但你有沒有發(fā)現(xiàn)一個問題呢序芦?使用上面的方式臭杰,本質(zhì)都是在一個線程執(zhí)行完成后再執(zhí)行另一個線程,包括我們的互斥鎖谚中,都是在 counter1
計算完成后再解鎖渴杆,然后 counter2
才獲取了控制權(quán),繼續(xù)執(zhí)行宪塔。這樣雖然解決了搶占資源的問題磁奖,但性能卻不夠高,看上去是多個線程某筐,實際上只有一個線程在執(zhí)行比搭,下一個線程需要等前一個線程執(zhí)行完成后再執(zhí)行。(其實 Python 中的多線程同一時間也只有一個線程在執(zhí)行南誊,后面將會講到 GIL身诺,說明情況蜜托。)
為了解決這個問題,有個很簡單的方案:我們?yōu)槊總€線程定義一個各自的全局變量然后在主線程中匯總不就行了嗎霉赡?現(xiàn)在對程序進(jìn)行一些改進(jìn):
from threading import Thread
counter1_num = 0
counter2_num = 0
# 讓 counter1 函數(shù)對 num 累加 100 萬次
def counter1():
global counter1_num
for i in range(1000000):
counter1_num += 1
print("counter1 計算出來的 num 的值是 %d"%counter1_num)
# 讓 counter2 函數(shù)對 num 累加 100 萬次
def counter2():
global counter2_num
for i in range(1000000):
counter2_num += 1
print("counter2 計算出來的 num 的值是 %d"%counter2_num)
# 主模塊中開啟線程
if __name__ == '__main__':
th1 = Thread(target = counter1)
th2 = Thread(target = counter2)
th1.start()
th2.start()
th1.join()
th2.join()
print("主線程中獲取的 num 的值是 %d"%(counter1_num + counter2_num))
運(yùn)行結(jié)果:
PS C:\Users\Charley\Desktop\py> python .\py.py
counter1 計算出來的 num 的值是 1000000
counter2 計算出來的 num 的值是 1000000
主線程中獲取的 num 的值是 2000000
PS C:\Users\Charley\Desktop\py>
除了使用這種方式橄务,我們還可以使用 threading
模塊中的一個 local
函數(shù),調(diào)用該函數(shù)返回一個對象同廉,每個線程中都可以使用這個對象仪糖,但是相對于每個線程柑司,這個對象都是一份獨立的副本迫肖,不會彼此覆蓋:
from threading import Thread,local
# 使用 local 函數(shù)創(chuàng)建一個變量
thead_local = local()
num = 0
# 讓 counter1 函數(shù)對 num 累加 100 萬次
def counter1():
global num
thead_local.num = 0
for i in range(1000000):
thead_local.num += 1
print("counter1 計算出來的 num 的值是 %d"%thead_local.num)
num += thead_local.num
# 讓 counter2 函數(shù)對 num 累加 100 萬次
def counter2():
global num
thead_local.num = 0
for i in range(1000000):
thead_local.num += 1
print("counter2 計算出來的 num 的值是 %d"%thead_local.num)
num += thead_local.num
# 主模塊中開啟線程
if __name__ == '__main__':
th1 = Thread(target = counter1)
th2 = Thread(target = counter2)
th1.start()
th2.start()
th1.join()
th2.join()
print("主線程中獲取的 num 的值是 %d"%num)
運(yùn)行效果:
PS C:\Users\Charley\Desktop\py> python .\py.py
counter1 計算出來的 num 的值是 1000000
counter2 計算出來的 num 的值是 1000000
主線程中獲取的 num 的值是 2000000
PS C:\Users\Charley\Desktop\py>
GIL
GIL 也叫全局解釋器鎖,是 Python 語言在實現(xiàn)多線程時的一種機(jī)制攒驰,也是影響 Python 多線程性能的一個重要因素蟆湖。
在說這個問題之前,我們先來看下 Python 多線程在單核 CPU玻粪、多核 CPU 情況下的表現(xiàn)隅津。我們使用這樣一份代碼:
from threading import Thread
def target():
while True:
pass
if __name__ == "__main__":
t1 = Thread(target = target)
t2 = Thread(target = target)
t1.start()
t2.start()
1)單核 CPU 下的 CPU 使用情況:
2)多核 CPU 下的 CPU 使用情況:
我們看到,在單核 CPU 的情況下劲室,CPU 被利用的很充分(100%)伦仍,而在多核 CPU 的情況下,CPU 利用的并不是太充分很洋,還有許多空閑充蓝。這就是 Python 的 GIL 機(jī)制,也是影響多線程性能的一個地方喉磁。
與此同時谓苟,我們來看一下多核 CPU 情況下多進(jìn)程對 CPU 的利用情況:
from multiprocessing import Process
def target():
while True:
pass
if __name__ == "__main__":
p1 = Process(target = target)
p2 = Process(target = target)
p1.start()
p2.start()
看一下多進(jìn)程對 CPU 的利用率情況:
通過這幾個測驗可以看出:Python 語言中,多進(jìn)程比多線程對 CPU 的利用率更高协怒。
GIL 導(dǎo)致 Python 多線程的性能降低原因是:在此機(jī)制下涝焙,(Python 中的多線程并不是同時執(zhí)行的,同一時間只有一個線程在執(zhí)行孕暇,不管有多少 CPU 核心都是如此仑撞,執(zhí)行下一個線程需要等待上一個線程從 CPU 中調(diào)度出來后才能執(zhí)行。這就是 Python 中多線程性能較低的原因妖滔。
要解決多線程 GIL 的弊端隧哮,可以有下面兩種方式:
- 盡量使用多進(jìn)程
- 使用 C語言或者其他沒有 GIL 機(jī)制的語言構(gòu)建核心模塊,然后在 Python 中導(dǎo)入
總結(jié)
本文主要講到了 Python 語言中的多線程實現(xiàn)铛楣,大體有以下幾個知識點:
- 多線程的創(chuàng)建
- 擴(kuò)展
Thread
類 - 多線程搶占資源的問題及集中解決方案
- 互斥鎖
local
- 多線程和多進(jìn)程的性能問題
- GIL
- GIL 問題的解決
完近迁。