多線程
什麼是python多線程
多線程是加速程序計算的有效方式,Python的多線程模塊 threading 是挺容易學習的。
線程在執行過程中與進程還是有區別的。每個獨立的進程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
下面通過一個實例學習:
import threading
import time
import subprocess
def thread_job():
print('T1 start\n')
for i in range(10):
time.sleep(0.1)
print('T1 finish\n')
def T2_job():
print('T2 start \n')
print('T2 finish\n')
def main():
# 通過 threading.Thread 創建一個線程,
# 參數1 是線程函數;注意這裏只是將函數的索引賦值給 target , 沒有括號
# 參數2 是該線程的名字
added_thread = threading.Thread(target=thread_job, name='T1')
thread2 = threading.Thread(target=T2_job, name='T2')
# 啓動線程
added_thread.start()
thread2.start()
# 阻塞調用線程直至線程的join() 方法被調用中止,後面的程序纔會執行
added_thread.join()
thread2.join()
print('all done\n')
# 打印出當前的線程變量,打印正在運行的線程數量,打印正在運行的線程的list
print(threading.current_thread())
print(threading.active_count())
print(threading.enumerate())
if __name__ == '__main__':
main()
上述程序運行結果:
T1 start
T2 start
T2 finish
T1 finish
all done
<_MainThread(MainThread, started 38104)>
1
[<_MainThread(MainThread, started 38104)>]
Process finished with exit code 0
可以看到當兩個線程的主函數執行完之後,後面的程序纔開始執行。打印 all done
。
常用的線程模塊及方法
Python通過兩個標準庫thread和threading提供對線程的支持。thread提供了低級別的、原始的線程以及一個簡單的鎖。
-
threading 模塊提供的其他方法:
-
threading.currentThread(): 返回當前的線程變量。
-
threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啓動後、結束前,不包括啓動前和終止後的線程。
-
threading.activeCount(): 返回正在運行的線程數量,與len(threading.enumerate())有相同的結果。
除了使用方法外,線程模塊同樣提供了Thread類來處理線程,Thread類提供了以下方法:
- run(): 用以表示線程活動的方法。
- start():啓動線程活動。
- join([time]): 等待至線程中止。這阻塞調用線程直至線程的join() 方法被調用中止-正常退出或者拋出未處理的異常-或者是可選的超時發生。
- isAlive(): 返回線程是否活動的。
- getName(): 返回線程名。
- setName(): 設置線程名。
存儲進程結果 Queue
由於線程函數中不能使用 return
語句,於是我們考慮將線程函數中要保存的值放到隊列中。
繼續看一個實例:
import threading
import time
from queue import Queue
def job(l, q):
for i in range(len(l)):
l[i] = l[i]**2
# return l
# 調用線程不能用return,因此我們將它放入隊列中
q.put(l)
def multithreading():
q = Queue()
# 使用一個list來存放創建的線程
threads = []
data = [[1,2,3],[3,4,5],[4,4,4],[5,5,5]]
# 通過一個循環建立四個線程
for i in range(4):
t = threading.Thread(target=job, args=(data[i], q))
t.start()
threads.append(t)
# 將循環中給的當前線程附加到主線程裏,即,執行完該線程才能往下執行
for thread in threads:
thread.join()
result = []
# 循壞四次取出隊列中的值
for _ in range(4):
result.append(q.get())
print(result)
if __name__ == '__main__':
multithreading()
多線程執行的效率?
在人們的常識中,多進程,多線程都是通過併發的方式充分利用硬件資源提高程序的運行效率,但是在python中卻不一定是這樣。相反,這個功能有點雞肋。
這是爲什麼呢?GIL
的存在使得多行程的運行效率真不一定高。GIL
待會說,先看多線程是不是雞肋,我們用代碼來說明。
import time
import threading
def decrement(n):
while n > 0:
n -= 1
start = time.time()
# 單線程
decrement(100000000)
cost = time.time() - start
print('single thread: ', cost)
# 多線程
start = time.time()
t1 = threading.Thread(target=decrement, args=[50000000])
t2 = threading.Thread(target=decrement, args=[50000000])
t1.start() # 啓動線程,執行任務
t2.start() # 同上
t1.join() # 主線程阻塞,直到t1執行完成,主線程繼續往後執行
t2.join() # 同上
cost = time.time() - start
print('multi thread:',cost)
運行結果;
single thread: 4.635588884353638
multi thread: 4.316659688949585
可以看到,使用多線程後效率並沒有提升,相反,有時效率反而會下降。
GIL
是什麼原因導致多線程不快反慢的呢?
原因就在於 GIL ,在 Cpython 解釋器(Python語言的主流解釋器)中,有一把全局解釋鎖(Global Interpreter Lock),在解釋器解釋執行 Python 代碼時,先要得到這把鎖,意味着,任何時候只可能有一個線程在執行代碼,其它線程要想獲得 CPU 執行代碼指令,就必須先獲得這把鎖,如果鎖被其它線程佔用了,那麼該線程就只能等待,直到佔有該鎖的線程釋放鎖纔有執行代碼指令的可能。
線程鎖 Lock
python解釋器在執行程序時,在任何時候只有一個線程在執行代碼。我們定義的多個線程是在不停的切換,只是速度快到我們以爲踏實多個線程並行,。那麼這種情況下可能會出現一個問題:如果我們定義一個全局變量,而我們的線程函數中也使用了該變量,那麼多個線程切換着執行可能會對該變量進行不同的運算。
這種情況下,就需要Python提供的進程鎖 lock
了。在線程函數中調用鎖,執行完在釋放,使得整個過程不收其他進程干擾。
一個關於線程鎖的示例:
import threading
def job1():
global A, lock
lock.acquire()
for i in range(5):
A += 1
print('job1 ', A)
lock.release()
def job2():
global A, lock
lock.acquire()
for i in range(10):
A += 10
print('job2 ', A)
lock.release()
if __name__ == '__main__':
lock = threading.Lock()
A = 0
t1 = threading.Thread(target=job1)
t2 = threading.Thread(target=job2)
t1.start()
t2.start()
# t2.join()
# t1.join()
執行結果:
job1 1
job1 2
job1 3
job1 4
job1 5
job2 15
job2 25
job2 35
job2 45
job2 55
job2 65
job2 75
job2 85
job2 95
job2 105
我們將線程函數中的結果打印出來,可以看到首先對全局變量A
進行+1
操作,循環打印5次。之後再線程函數job2
中執行+10
操作。
可以將線程鎖去掉,其他部分保持不變,執行原程序,觀察打印的結果是否還是如此整齊一致?
歡迎關注我的微信公衆號,談風月之餘談技術