Python 中的多線程
什麼是線程
一個進程中包括多個線程,線程是 CPU 調度和分派的基本單位,是進程中執行運算的最小單位,真正在 CPU 上運行的是線程,可以與同一個進程中的其他線程共享進程的全部資源
Python 中實現多線程
Python 中有兩種方式牀架多線程,一種是調用底層的 thread
模塊(Python3 中已棄用),另一種是使用threading
模塊,下面我說的也是使用這個模塊實現多線程的方法
從形式上將,多線程的實現和多進程的實現十分類似,threading
模塊提供了Thread
類來創建線程,同Process
類一樣,我們可以通過直接調用或創建子類來繼承這兩種方式來創建線程
使用 Thread 實現多線程
直接調用threading
,模塊的Thread
類來創建線程十分簡單,使用threading.Thread([target], [(item1, item2, ...)])
方法,target
爲目標函數名稱(如果有目標函數的話),後邊爲參數元組,用來傳遞函數參數
線程創建好後,調用Thread.start()
方法,就可以運行線程,如果沒有目標函數,start()
會自動執行Thread
類中的run()
方法,示例如下:
import threading
import time
def saySorry():
time.sleep(5)
print('I am sorry')
if __name__ == '__main__':
for i in range(5):
print('create %i Thread' % i)
t = threading.Thread(target=saySorry)
t.start()
運行結果:
create 0 Thread
create 1 Thread
create 2 Thread
create 3 Thread
create 4 Thread
I am sorry
I am sorry
I am sorry
I am sorry
I am sorry
上邊的程序也證明了,程序在創建了子線程後,不會等待線程執行完畢,而是會繼續向下執行,而當主程序全部執行完畢後,卻會等待所有子線程執行完畢再結束,這點和進程有些區別
繼承 Thread 實現多線程
另外一種方式是通過創建繼承Thread
類的子類來實現多線程,這樣做的好處就是可以將線程要運行的代碼全部放入run
函數中,用起來更方便,示例如下:
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
msg = 'I am ' + self.name + ' @ ' + str(i)
print(msg)
if __name__ == '__main__':
t = MyThread()
t.start()
運行結果:
I am Thread-1 @ 0
I am Thread-1 @ 1
I am Thread-1 @ 2
Python 多線程中全局變量和非全局變量的使用問題
開始就說過,線程可以和同一個進程中的其他線程共享進程的全部資源,那麼在多線程程序中,各線程對全局變量和非全局變量的使用到底是怎樣的呢
非全局變量
非全局變量在多線程中是不會被共享的,這就像是假設有一個存在局部變量a
的函數,當程序調用兩次這個函數時,每次調用所產生的局部變量a
都是一個新的變量,不會受另一個函數執行的干擾,示例如下:
from threading import Thread
import threading
def test1():
str = threading.current_thread().name
g_num = 100
if str == 'Thread-1':
print(str)
g_num += 1
else:
print(str)
g_num -= 1
print('-----test1----- g_num = %d' % g_num)
p1 = Thread(target=test1)
p1.start()
p2 = Thread(target=test1)
p2.start()
運行結果:
Thread-1
-----test1----- g_num = 101
Thread-2
-----test1----- g_num = 99
可以看到,局部變量g_num
並不會因爲另一個線程中的同名函數而收到影響
全局變量
在多線程中,全局變量是可以在各線程間共享的,這也就是說,線程間通信不需要通過管道、內存映射等方法,只需要使用一個全局變量(同一個進程中的共享資源)便可以,示例如下:
from threading import Thread
g_flag = 0
g_num = 0
def test1():
global g_num
for i in range(1000000):
g_num += 1
print("---test1--- g_num = %d" % g_num)
def test2():
global g_num
for i in range(1000000):
g_num += 1
print("---test2--- g_num = %d " % g_num)
p1 = Thread(target=test1)
p1.start()
p2 = Thread(target=test2)
p2.start()
print("---g_num= %d ---" % g_num)
運行結果:
---g_num= 372639 ---
---test1--- g_num = 1456098
---test2--- g_num = 1596586
到這裏,我們可以發現一個問題,正常來講,g_num
最後的值不應給是2000000
嗎,爲什麼不是呢?這就是多線程使用全局變量時有可能出現的 bug,請繼續閱讀!
Python 多線程中如何防止使用全局變量出現 bug(輪詢和互斥鎖)
通過剛纔在多線程中使用全局變量我們發現,當代碼邏輯稍微複雜一些時,在兩個線程中同時使用一個全局變量會出現問題,是什麼導致了這個問題呢?
從代碼中我們可以發現g_num += 1
這句代碼,實際上是g_num + 1
和將其結果賦給g_num
兩步,正是因爲這連續的兩次對全局變量的操作造成了這個問題
當一個線程執行到g_num + 1
這步後,cpu 有可能會轉頭去處理另一個線程,另一個線程也運行了g_num + 1
,當 cpu 再回頭執行第一個線程時,g_num
已經不止被運算過一次了
那麼怎麼避免這樣的情況發生呢,只能是如果存在對全局變量變量值的修改時,我們要優先運行一個線程,當它結束修改後,再允許另一個線程去訪問這個局部變量,下面提供兩種方式,輪詢和互斥鎖
輪詢
顧名思義,輪詢的意思就是反覆詢問,抽象起來理解就是,我們可以設置另一個用來作爲目標值的全局變量,兩個線程執行的條件根據目標值的不同而不同,當目標值滿足一個線程執行時,其他線程就會一直處在一個堵塞的過程,它會一直詢問目標值是否符合自己,當上一個線程結束時,這個線程會將目標值修改,這樣下一個符合目標值的線程就會運行,示例如下:
from threading import Thread
g_flag = 0
g_num = 0
def test1():
global g_num
global g_flag
if g_flag == 0:
for i in range(1000000):
g_num += 1
g_flag = 1
print("---test1--- g_num = %d" % g_num)
def test2():
global g_num
global g_flag
while True:
if g_flag != 0:
for i in range(1000000):
g_num += 1
break
print("---test2--- g_num = %d " % g_num)
p1 = Thread(target=test1)
p1.start()
p2 = Thread(target=test2)
p2.start()
print("---g_num= %d ---" % g_num)
運行結果:
---g_num= 181893 ---
---test1--- g_num = 1000000
---test2--- g_num = 2000000
如結果所示,線程之間使用全局變量的 bug 已經解決,但是輪詢的方法十分消耗資源,堵塞的線程其實一直都處在一個死循環的狀態佔用系統資源
互斥鎖
相比而言,互斥鎖就是一種比較優化的方法,互斥鎖會使用threading
模塊的Lock
類
互斥鎖的思想是,當一個線程運行時,它會給它需要的這部分資源上鎖,這樣同樣使用這把鎖的其他線程全部都會被堵塞,但被互斥鎖所堵塞的線程不會佔用系統資源,它們會處在睡眠狀態,當運行的線程用完被鎖的這部分資源後,它會解鎖,這時其他線程就會被喚醒來搶佔 cpu 資源,得到資源的線程會再次上鎖,達到多線程下全局變量的訪問安全,示例如下:
from threading import Thread
from threading import Lock
g_num = 0
mutex = Lock()
def test1():
global g_num
mutex.acquire()
for i in range(1000000):
g_num += 1
mutex.release()
print("---test1--- g_num = %d" % g_num)
def test2():
global g_num
mutex.acquire()
for i in range(1000000):
g_num += 1
mutex.release()
print("---test2--- g_num = %d " % g_num)
p1 = Thread(target=test1)
p1.start()
p2 = Thread(target=test2)
p2.start()
print("---g_num= %d ---" % g_num)
運行結果:
---g_num= 200009 ---
---test1--- g_num = 1000000
---test2--- g_num = 2000000
互斥鎖的方法說明:
# 創建鎖
mutex = threading.Lock()
# 上鎖,blocking 爲 True 表示堵塞
mutex.acquire([blocking])
# 解鎖,只要開了鎖,那麼接下來會讓所有因爲這個鎖而被阻塞的線程搶着上鎖
mutex.release()
使用互斥鎖時要注意,爲了提高運算效率,上鎖的資源越少,運算的效率越高
另外,線程等待解鎖的方式不是通過輪詢,二十通過通知,線程會睡眠,等待喚醒的通知,所以互斥鎖較輪詢來講更爲優化