【莫煩】Threading 多執行緒教程
1.1 什麼是多執行緒 Threading
同時分批做同一件事情,以提高運算效率
1.2 新增執行緒 Thread
新增執行緒
本節我們來學習threading模組的一些基本操作,如獲取執行緒數,新增執行緒等。首先別忘了匯入模組:
import threading
獲取已啟用的執行緒數
threading.active_count()
# 2
檢視所有執行緒資訊
threading.enumerate()
# [<_MainThread(MainThread, started 140736011932608)>, <Thread(SockThread, started daemon 123145376751616)>]
輸出的結果是一個<_MainThread(...)>
帶多個<Thread(...)>
。
檢視現在正在執行的執行緒
threading.current_thread()
# <_MainThread(MainThread, started 140736011932608)>
新增執行緒,threading.Thread()
接收引數target
代表這個執行緒要完成的任務,需自行定義
def thread_job():
print('This is a thread of %s' % threading.current_thread())
def main():
thread = threading.Thread(target=thread_job,) # 定義執行緒
thread.start() # 讓執行緒開始工作
完整程式與執行結果
import threading
def thread_job():
print('This is an added Thread,number is %s'% threading.current_thread())
def main ():
added_thread=threading.Thread(target=thread_job)
added_thread.start()
print(threading.active_count())
print(threading.enumerate())
print(threading.current_thread())
if __name__=='__main__':
main()
"""
This is an added Thread,number is <Thread(Thread-1, started 6904)>
2
[<_MainThread(MainThread, started 12484)>, <Thread(Thread-1, started 6904)>]
<_MainThread(MainThread, started 12484)>
"""
1.3 join 功能
- 當一個程式啟動之後,會預設產生一個主執行緒,因為執行緒是程式執行流的最小單元,當設定多執行緒時,主執行緒會建立多個子執行緒,在python中,預設情況下(其實就是setDaemon(False)),主執行緒執行完自己的任務以後,就退出了,此時子執行緒會繼續執行自己的任務,直到自己的任務結束.
- 當我們使用setDaemon(True)方法,設定子執行緒為守護執行緒時,主執行緒一旦執行結束,則全部執行緒全部被終止執行,可能出現的情況就是,子執行緒的任務還沒有完全執行結束,就被迫停止。
- 此時join的作用就凸顯出來了,join所完成的工作就是執行緒同步,即主執行緒任務結束之後,進入阻塞狀態,一直等待其他的子執行緒執行結束之後,主執行緒在終止。
- join有一個timeout引數:當設定守護執行緒時,含義是主執行緒對於子執行緒等待timeout的時間將會殺死該子執行緒,最後退出程式。所以說,如果有10個子執行緒,全部的等待時間就是每個timeout的累加和。簡單的來說,就是給每個子執行緒一個timeout的時間,讓他去執行,時間一到,不管任務有沒有完成,直接殺死。沒有設定守護執行緒時,主執行緒將會等待timeout的累加和這樣的一段時間,時間一到,主執行緒結束,但是並沒有殺死子執行緒,子執行緒依然可以繼續執行,直到子執行緒全部結束,程式退出。
不加 join() 的結果
我們讓T1
執行緒工作的耗時增加.
import threading
import time
def thread_job():
print("T1 start\n")
for i in range(10):
time.sleep(0.1) # 任務間隔0.1s
print("T1 finish\n")
added_thread = threading.Thread(target=thread_job, name='T1')
added_thread.start()
print("all done\n")
預想中輸出的結果是否為:
T1 start
T1 finish
all done
但實際卻是:
T1 start
all done
T1 finish
加入 join() 的結果
執行緒任務還未完成便輸出all done
。如果要遵循順序,可以在啟動執行緒後對它呼叫join
:
added_thread.start()
added_thread.join()
print("all done\n")
使用join
對控制多個執行緒的執行順序非常關鍵。舉個例子,假設我們現在再加一個執行緒T2
,T2
的任務量較小,會比T1
更快完成:
def T1_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")
thread_1 = threading.Thread(target=T1_job, name='T1')
thread_2 = threading.Thread(target=T2_job, name='T2')
thread_1.start() # 開啟T1
thread_2.start() # 開啟T2
print("all done\n")
輸出的”一種”結果是:
T1 start
T2 start
T2 finish
all done
T1 finish
現在T1
和T2
都沒有join
,注意這裡說”一種”是因為all done
的出現完全取決於兩個執行緒的執行速度, 完全有可能T2 finish
出現在all done
之後。這種雜亂的執行方式是我們不能忍受的,因此要使用join
加以控制。
我們試試在T1
啟動後,T2
啟動前加上thread_1.join()
:
thread_1.start()
thread_1.join() # notice the difference!
thread_2.start()
print("all done\n")
輸出結果:
T1 start
T1 finish
T2 start
all done
T2 finish
可以看到,T2
會等待T1
結束後才開始執行。
如果我們在T2
啟動後放上thread_1.join()
會怎麼樣呢?
thread_1.start()
thread_2.start()
thread_1.join() # notice the difference!
print("all done\n")
``
輸出結果:
```py
T1 start
T2 start
T2 finish
T1 finish
all done
T2
在T1
之後啟動,並且因為T2
任務量小會在T1
之前完成;而T1
也因為加了join
,all done
在它完成後才顯示。
你也可以新增thread_2.join()
進行嘗試,但為了規避不必要的麻煩,推薦如下這種1221
的V型排布:
thread_1.start() # start T1
thread_2.start() # start T2
thread_2.join() # join for T2
thread_1.join() # join for T1
print("all done\n")
"""
T1 start
T2 start
T2 finish
T1 finish
all done
"""
1.4 儲存程式結果 Queue
程式碼實現功能,將資料列表中的資料傳入,使用四個執行緒處理,將結果儲存在Queue
中,執行緒執行完後,從Queue
中獲取儲存的結果
匯入執行緒,佇列的標準模組
import threading
import time
from queue import Queue
定義一個被多執行緒呼叫的函式
函式的引數是一個列表l和一個佇列q,函式的功能是,對列表的每個元素進行平方計算,將結果儲存在佇列中
def job(l,q):
for i in range (len(l)):
l[i] = l[i]**2
q.put(l) #多執行緒呼叫的函式不能用return返回值
定義一個多執行緒函式
在多執行緒函式中定義一個Queue
,用來儲存返回值,代替return
,定義一個多執行緒列表,初始化一個多維資料列表,用來處理:
def multithreading():
q =Queue() #q中存放返回值,代替return的返回值
threads = [] #建立一個陣列作為程式列表,以把建立好的執行緒裝到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)) #Thread首字母要大寫,被呼叫的job函式沒有括號,只是一個索引,引數在後面
t.start()#開始執行緒
threads.append(t) #把每個執行緒append到執行緒列表中
分別join
四個執行緒到主執行緒
for thread in threads: #thread可以用其他符號替代 如t、i
thread.join()
定義一個空的列表results
,將四個線執行後儲存在佇列中的結果返回給空列表results
results = []
for _ in range(4):
results.append(q.get()) #q.get()按順序從q中拿出一個值
print(results)
完整的程式碼
import threading
import time
from queue import Queue
def job(l,q):
for i in range (len(l)):
l[i] = l[i]**2
q.put(l)
def multithreading():
q =Queue()
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()
results = []
for _ in range(4):
results.append(q.get())
print(results)
if __name___=='__main__':
multithreading()
最後執行結果為:
[[1, 4, 9], [9, 16, 25], [16, 16, 16], [25, 25, 25]]
1.5 GIL 不一定有效率
這次我們來看看為什麼說 python 的多執行緒 threading 有時候並不是特別理想. 最主要的原因是就是, Python 的設計上, 有一個必要的環節, 就是 Global Interpreter Lock (GIL). 這個東西讓 Python 還是一次性只能處理一個東西.
我從這裡摘抄了一段對於 GIL 的解釋.
儘管Python完全支援多執行緒程式設計, 但是直譯器的C語言實現部分在完全並行執行時並不是執行緒安全的。 實際上,直譯器被一個全域性直譯器鎖保護著,它確保任何時候都只有一個Python執行緒執行。 GIL最大的問題就是Python的多執行緒程式並不能利用多核CPU的優勢 (比如一個使用了多個執行緒的計算密集型程式只會在一個單CPU上面執行)。
在討論普通的GIL之前,有一點要強調的是GIL只會影響到那些嚴重依賴CPU的程式(比如計算型的)。 如果你的程式大部分只會涉及到I/O,比如網路互動,那麼使用多執行緒就很合適, 因為它們大部分時間都在等待。實際上,你完全可以放心的建立幾千個Python執行緒, 現代作業系統執行這麼多執行緒沒有任何壓力,沒啥可擔心的。
測試 GIL
我們建立一個 job, 分別用 threading 和 一般的方式執行這段程式. 並且建立一個 list 來存放我們要處理的資料. 在 Normal 的時候, 我們這個 list 擴充套件4倍, 在 threading 的時候, 我們建立4個執行緒, 並對執行時間進行對比.
import threading
from queue import Queue
import copy
import time
def job(l, q):
res = sum(l)
q.put(res)
def multithreading(l):
q = Queue()
threads = []
for i in range(4):
t = threading.Thread(target=job, args=(copy.copy(l), q), name='T%i' % i)
t.start()
threads.append(t)
[t.join() for t in threads]
total = 0
for _ in range(4):
total += q.get()
print(total)
def normal(l):
total = sum(l)
print(total)
if __name__ == '__main__':
l = list(range(1000000))
s_t = time.time()
normal(l*4)
print('normal: ',time.time()-s_t)
s_t = time.time()
multithreading(l)
print('multithreading: ', time.time()-s_t)
如果你成功執行整套程式, 你大概會有這樣的輸出. 我們的運算結果沒錯, 所以程式 threading 和 Normal 執行了一樣多次的運算. 但是我們發現 threading 卻沒有快多少, 按理來說, 我們預期會要快3-4倍, 因為有建立4個執行緒, 但是並沒有. 這就是其中的 GIL 在作怪.
1999998000000
normal: 0.10034608840942383
1999998000000
multithreading: 0.08421492576599121
1.6 執行緒鎖 Lock
匯入執行緒標準模組
import threading
不使用 Lock 的情況
函式一:全域性變數A的值每次加1,迴圈10次,並列印
def job1():
global A
for i in range(10):
A+=1
print('job1',A)
函式二:全域性變數A的值每次加10,迴圈10次,並列印
def job2():
global A
for i in range(10):
A+=10
print('job2',A)
主函式:定義兩個執行緒,分別執行函式一和函式二
if __name__== '__main__':
A=0
t1=threading.Thread(target=job1)
t2=threading.Thread(target=job2)
t1.start()
t2.start()
t1.join()
t2.join()
完整程式碼:
import threading
def job1():
global A
for i in range(10):
A+=1
print('job1',A)
def job2():
global A
for i in range(10):
A+=10
print('job2',A)
if __name__== '__main__':
lock=threading.Lock()
A=0
t1=threading.Thread(target=job1)
t2=threading.Thread(target=job2)
t1.start()
t2.start()
t1.join()
t2.join()
執行結果(在spyder編譯器下執行的列印結果):
job1job2 11
job2 21
job2 31
job2 41
job2 51
job2 61
job2 71
job2 81
job2 91
job2 101
1
job1 102
job1 103
job1 104
job1 105
job1 106
job1 107
job1 108
job1 109
job1 110
可以看出,列印的結果非常混亂
使用 Lock 的情況
lock在不同執行緒使用同一共享記憶體時,能夠確保執行緒之間互不影響,使用lock的方法是, 在每個執行緒執行運算修改共享記憶體之前,執行lock.acquire()
將共享記憶體上鎖, 確保當前執行緒執行時,記憶體不會被其他執行緒訪問,執行運算完畢後,使用lock.release()
將鎖開啟, 保證其他的執行緒可以使用該共享記憶體。
函式一和函式二加鎖
def job1():
global A,lock
lock.acquire()
for i in range(10):
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()
主函式中定義一個Lock
if __name__== '__main__':
lock=threading.Lock()
A=0
t1=threading.Thread(target=job1)
t2=threading.Thread(target=job2)
t1.start()
t2.start()
t1.join()
t2.join()
完整的程式碼
import threading
def job1():
global A,lock
lock.acquire()
for i in range(10):
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()
t1.join()
t2.join()
執行結果
job1 1
job1 2
job1 3
job1 4
job1 5
job1 6
job1 7
job1 8
job1 9
job1 10
job2 20
job2 30
job2 40
job2 50
job2 60
job2 70
job2 80
job2 90
job2 100
job2 110
從列印結果來看,使用lock後,一個一個執行緒執行完。使用lock和不使用lock,最後列印輸出的結果是不同的。
相關文章
- Python 之 threading(多執行緒)用法教程Pythonthread執行緒
- threading多執行緒資源加鎖thread執行緒
- python之 threading(多執行緒)模組Pythonthread執行緒
- python中的socket+threading多執行緒Pythonthread執行緒
- Python多執行緒之_thread與threading模組Python執行緒thread
- 【莫煩】Multiprocessing 多程式
- python threading 執行緒原理實驗Pythonthread執行緒
- 【莫煩】python基礎教程Python
- 莫煩Tensorflow教程(1~14)
- Android入門教程 | 多執行緒Android執行緒
- 多執行緒和多執行緒同步執行緒
- 多執行緒--執行緒管理執行緒
- 執行緒與多執行緒執行緒
- 多執行緒【執行緒池】執行緒
- C++多執行緒基礎教程C++執行緒
- Java入門教程十三(多執行緒)Java執行緒
- Java多執行緒-執行緒中止Java執行緒
- 多執行緒之初識執行緒執行緒
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- 多執行緒系列(1),多執行緒基礎執行緒
- a、多執行緒執行緒
- 【莫煩】Python MatplotlibPython
- 多執行緒系列之 執行緒安全執行緒
- iOS 多執行緒之執行緒安全iOS執行緒
- Java多執行緒之執行緒中止Java執行緒
- Android多執行緒之執行緒池Android執行緒
- Java多執行緒-執行緒狀態Java執行緒
- Java多執行緒-執行緒通訊Java執行緒
- kuangshenshuo-多執行緒-執行緒池執行緒
- java 多執行緒守護執行緒Java執行緒
- Java多執行緒(2)執行緒鎖Java執行緒
- 多執行緒之手撕執行緒池執行緒
- java多執行緒9:執行緒池Java執行緒
- 【java多執行緒】(二)執行緒停止Java執行緒
- 執行緒以及多執行緒,多程式的選擇執行緒
- 多執行緒學習一(多執行緒基礎)執行緒
- Java多執行緒(一)多執行緒入門篇Java執行緒
- 多執行緒,多程式執行緒