程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

python + threading模塊 多線程學習

編輯:Python

python 多線程

  • Python 線程threading
    • Python多線程適用於I/O密集型
    • Python多線程的工作過程
    • 構造函數
    • 線程函數
      • start()
      • join(timeout=None)
    • 創建線程
    • 線程加鎖
    • 5種線程鎖
    • join函數
      • 第一種:Python多線程默認情況(非守護線程)
      • 第二種:守護線程
      • 第三種:加入join方法設置同步(非守護線程,且join不設置超時)
      • 第四種:加入join方法設置同步(非守護線程,join設置超時)
      • 第五種:加入join方法設置同步(*守護線程*,join設置超時)

Python 線程threading

Python多線程適用於I/O密集型

GIL的全稱是Global Interpreter Lock(全局解釋器鎖),為了數據安全,GIL保證同一時間只能有一個線程拿到數據。所以,在python中,同時只能執行一個線程。

IO密集型,多線程能夠有效提昇效率( 單線程下有IO操作會進行IO等待,造成不必要的時間浪費,而開啟多線程能在線程A等待時,自動切換到線程B,可以不浪費CPU的資源,從而能提昇程序執行效率 )。所以python多線程對IO密集型代碼比較友好

CPU密集型( 各種循環處理、計算等等 ),由於計算工作多,計時器很快就會達到閾值,然後觸發GIL的釋放與再競爭( 多個線程來回切換當然是需要消耗資源的 ),所以python多線程對CPU密集型代碼並不友好

Python多線程的工作過程

Python在使用多線程的時候,調用的是c語言的原生線程

  1. 拿到公共數據
  2. 申請GIL(全局解釋器鎖)
  3. python解釋器調用os原生線程
  4. os操作cpu執行運算
  5. 當該線程執行時間到後,無論運算是否已經執行完,GIL都被要求釋放
  6. 由其他進程重複上面的過程
  7. 等其他進程執行完後,又會切換到之前的線程(從他記錄的上下文繼續執行),整個過程是每個線程執行自己的運算,當執行時間到就進行切換(context switch)。

筆記:有些沒看懂?

構造函數

threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

調用這個構造函數時,必需帶有關鍵字參數。參數如下:

  1. group 應該為 None;為了日後擴展 ThreadGroup 類實現而保留。

  2. target 是用於 run() 方法調用的可調用對象(一個函數)。默認是 None,錶示不需要調用任何方法。

  3. name 是線程名稱。默認情況下,由 “Thread-N” 格式構成一個唯一的名稱,其中 N 是小的十進制數。多個線程可以賦予相同的名稱

  4. args 是用於調用目標函數的參數元組。默認是 ()。

  5. kwargs 是用於調用目標函數的關鍵字參數字典。默認是 {}。

  6. daemon 參數如果不是 None,將顯式地設置該線程是否為守護模式。 如果是 None (默認值),線程將繼承當前線程的守護模式屬性
    3.3 版及以上才具有該屬性。
    注意:一定要在調用 start() 前設置好,不然會拋出 RuntimeError
    初始值繼承於創建線程;主線程不是守護線程,因此主線程創建的所有線程默認都是 daemon = False。
    當沒有存活的非守護線程時,整個Python程序才會退出。

  7. 如果子類型**重載了構造函數,**它一定要確保在做任何事前,先發起調用基類構造器(Thread.init())。

名詞解釋:守護模式

有一種線程,它是在後臺運行的,它的任務是為其他線程提供服務,這種線程被稱為“後臺線程(Daemon Thread)”,又稱為“守護線程”或“精靈線程”。Python 解釋器的垃圾回收線程就是典型的後臺線程。
後臺線程有一個特征,如果所有的前臺線程都死亡了,那麼後臺線程會自動死亡。

線程函數

start()

開始線程活動。

它在一個線程裏最多只能被調用一次。它安排對象的 run() 方法在一個獨立的控制進程中調用。

如果同一個線程對象中調用這個方法的次數大於一次,會拋出 RuntimeError

join(timeout=None)

等待,直到線程終結

這會阻塞調用這個方法的線程,直到被調用 join() 的線程終結。

  • 不管是正常終結
  • 還是拋出未處理異常
  • 或者直到發生超時,超時選項是可選的。

timeout 參數存在而且不是 None 時,它應該是一個用於指定操作超時的以為單比特的浮點數(或者分數)。
因為 join() 總是返回 None ,所以你一定要在 join() 後調用is_alive()才能判斷是否發生超時 ,如果線程仍然存活,則 join() 超時。

當 timeout 參數不存在或者是 None ,這個操作會阻塞直到線程終結

一個線程可以被 join() 很多次

  • 如果嘗試加入當前線程會導致死鎖, join() 會引起 RuntimeError 異常。
  • 如果嘗試 join() 一個尚未開始的線程,也會拋出相同的異常。

創建線程

import threading # 線程模塊
import time
def run(n):
print("task", n)
time.sleep(1)
print('2s')
time.sleep(1)
print('3s')
if __name__ == '__main__':
th = threading.Thread(target=run,name="thread_1" args=("thread 1",), daemon=True) # 創建線程
# 把子進程設置為守護線程,**必須在start()之前設置
th.setDaemon(True) # 設置守護線程,其實在創建時已經 設置了 daemon=True
th.start()
# 設置主線程等待子線程結束
th.join()
print("end")

線程加鎖

由於線程之間是進行隨機調度,並且每個線程可能只執行n條執行之後,當多個線程同時修改同一條數據時可能會出現髒數據筆記:Python基礎數據類型、列錶、元組、字典都是線程安全的,因此不會導致程序崩潰,但會導致數據出現未知值,即髒數據),

所以出現了線程鎖,即同一時刻允許一個線程執行操作。線程鎖用於鎖定資源,可以定義多個鎖,像下面的代碼,當需要獨占某一個資源時,任何一個鎖都可以鎖定這個資源,就好比你用不同的鎖都可以把這個相同的門鎖住一樣。

由於線程之間是進行隨機調度的,如果有多個線程同時操作一個對象,並且沒有很好地保護該對象,會造成程序結果的不可預期,我們因此也稱為“線程不安全”。
為了防止上面情況的發生,就出現了鎖。

5種線程鎖

  • 同步鎖
  • 遞歸鎖
  • 條件鎖
  • 事件鎖
  • 信號量鎖

我在5種Python線程鎖中有詳細講述。

join函數


概念補充

  • 在Python多線程編程中,join方法的作用是線程同步
  • 守護線程,是為守護別人而存在,當設置為守護線程後,被守護的主線程不存在後,守護線程也自然不存在

以下分5種不同的形式解釋join在多線程編程中的用處

第一種:Python多線程默認情況(非守護線程)

Python多線程的默認情況(設置線程setDaemon(False)),主線程執行完自己的任務以後,就退出了,此時子線程會繼續執行自己的任務,直到自己的任務結束

筆記:setDaemon(False) 即 該線程被設置為非守護線程;主程序退出後,子線程不會自動退出。

import threading, time
def doWaiting1():
print('start waiting1: ' + time.strftime('%H:%M:%S') + "\n")
time.sleep(3)
print("線程1奉命報道")
print('stop waiting1: ' + time.strftime('%H:%M:%S') + "\n")
def doWaiting2():
print('start waiting2: ' + time.strftime('%H:%M:%S') + "\n")
time.sleep(8)
print("線程2奉命報道")
print('stop waiting2: ', time.strftime('%H:%M:%S') + "\n")
tsk = [] # 線程列錶
# 創建並開啟線程1
thread1 = threading.Thread(target = doWaiting1)
thread1.start() # start()函數 實際調用 RUN()函數(Python調用的是 C語言的線程)
tsk.append(thread1)
# 創建並開啟線程2
thread2 = threading.Thread(target = doWaiting2)
thread2.start()
tsk.append(thread2)
# 計時程序
print('start join: ' + time.strftime('%H:%M:%S') )
print('end join: ' + time.strftime('%H:%M:%S') )

運行結果

start waiting1: 20:03:30
start waiting2: 20:03:30
start join: 20:03:30
end join: 20:03:30 # 此處主線程已經結束,而子線程還在繼續工作
線程1奉命報道 # 子線程1 繼續在工作
stop waiting1: 20:03:33
線程2奉命報道
stop waiting2: 20:03:38

結論

  1. 計時程序屬於主線程,整個主線程在開啟線程1和線程2後,進入計時模塊,主線程結束
  2. 主線程結束,但並沒有影響線程1和線程2的運行,故後面線程1和線程2仍然跑來報道,至此整個程序才完全結束

第二種:守護線程

開啟線程的**setDaemon(True),**設置子線程為守護線程,實現主程序結束,子程序立馬全部結束功能

import threading, time
def doWaiting1():
print('start waiting1: ' + time.strftime('%H:%M:%S') + "\n")
time.sleep(3)
print("線程1奉命報道")
print('stop waiting1: ' + time.strftime('%H:%M:%S') + "\n")
def doWaiting2():
print('start waiting2: ' + time.strftime('%H:%M:%S') + "\n")
time.sleep(8)
print("線程2奉命報道")
print('stop waiting2: ', time.strftime('%H:%M:%S') + "\n")
tsk = []
# 創建並開啟線程1
thread1 = threading.Thread(target = doWaiting1)
thread1.setDaemon(True)
thread1.start()
tsk.append(thread1)
# 創建並開啟線程2
thread2 = threading.Thread(target = doWaiting2)
thread2.setDaemon(True)
thread2.start()
tsk.append(thread2)
print('start join: ' + time.strftime('%H:%M:%S') )
print('end join: ' + time.strftime('%H:%M:%S') )

運行結果:

start waiting1: 20:10:04
start waiting2: 20:10:04
start join: 20:10:04
end join: 20:10:04 # 主線程結束後,子線程立刻結束,無論處於什麼狀態。

結論

  1. 主線程結束後,無論子線程1,2是否運行完成,都結束不再往下繼續運行

第三種:加入join方法設置同步(非守護線程,且join不設置超時)

非守護線程,主程序將一直等待子程序全部運行完成才結束

import threading, time
def doWaiting1():
print('start waiting1: ' + time.strftime('%H:%M:%S') + "\n")
time.sleep(3)
print("線程1奉命報道")
print('stop waiting1: ' + time.strftime('%H:%M:%S') + "\n")
def doWaiting2():
print('start waiting2: ' + time.strftime('%H:%M:%S') + "\n")
time.sleep(8)
print("線程2奉命報道")
print('stop waiting2: ', time.strftime('%H:%M:%S') + "\n")
tsk = []
# 創建並開啟線程1
thread1 = threading.Thread(target = doWaiting1)
thread1.start()
tsk.append(thread1)
# 創建並開啟線程2
thread2 = threading.Thread(target = doWaiting2)
thread2.start()
tsk.append(thread2)
print('start join: ' + time.strftime('%H:%M:%S') )
for t in tsk:
print('%s線程到了'%t)
t.join() # 線程join() 即:兩個線程再次回合,全部完成後才能繼續進行下一行
print('end join: ' + time.strftime('%H:%M:%S') )

運行結果:

start waiting1: 20:14:35
start waiting2: 20:14:35
start join: 20:14:35
<Thread(Thread-1, started 19648)>線程到了
線程1奉命報道
stop waiting1: 20:14:38
<Thread(Thread-2, started 24056)>線程到了
線程2奉命報道
stop waiting2: 20:14:43
end join: 20:14:43 # 兩個線程執行完畢後,才能運行至此

結論:

  1. 使用join函數,主線程將被阻塞(即:主線程,指定是創建子線程的線程),一直等待被使用了join方法的線程運行完成
  2. start join 是在35秒,stop waiting1在38秒,剛好sleep了3秒,stop waiting2是43秒,剛好sleep了8秒,這也說明,線程1和2是基本同時運行的,但由於執行所消耗的時間不一致,所以阻塞所用的時間也是不一樣的,最終end join時間是最後線程運行完,整個程序就中止在43秒
  3. 所有的線程放入一個列錶,通過循環對列錶中的所有線程使用join方法判斷,也是為了保證全部子線程都能全部運行完成,主線程才退出

第四種:加入join方法設置同步(非守護線程,join設置超時)

join設置timeout數值,判斷等待多後子線程還沒有完成,則主線程不再等待

筆記:join設置超時後,判斷依據為 子線程執行完畢 | 超時 (邏輯或的關系),即兩個條件誰先為真,就向下執行。

import threading, time
def doWaiting1():
print('start waiting1: ' + time.strftime('%H:%M:%S') + "\n")
time.sleep(2)
print("線程1奉命報道")
print('stop waiting1: ' + time.strftime('%H:%M:%S') + "\n")
def doWaiting2():
print('start waiting2: ' + time.strftime('%H:%M:%S') + "\n")
time.sleep(8)
print("線程2奉命報道")
print('stop waiting2: ', time.strftime('%H:%M:%S') + "\n")
tsk = []
# 創建並開啟線程1
thread1 = threading.Thread(target = doWaiting1)
thread1.start()
tsk.append(thread1)
# 創建並開啟線程2
thread2 = threading.Thread(target = doWaiting2)
thread2.start()
tsk.append(thread2)
print('start join: ' + time.strftime('%H:%M:%S') )
for t in tsk:
print("開始:"+time.strftime('%H:%M:%S'))
print('%s線程到了'%t)
t.join(5)
print("結束:" + time.strftime('%H:%M:%S'))
print('end join: ' + time.strftime('%H:%M:%S') )

運行結果:

start waiting1: 21:14:25
start waiting2: 21:14:25
start join: 21:14:25
開始:21:14:25
<Thread(Thread-1, started 22348)>線程到了
線程1奉命報道
stop waiting1: 21:14:27
結束:21:14:27
開始:21:14:27
<Thread(Thread-2, started 13164)>線程到了
結束:21:14:32
end join: 21:14:32
線程2奉命報道
stop waiting2: 21:14:33

結論

  1. 給join設置等待時間後,超過了等待時間後,主線程終止但不影響子線程繼續運行等子線程全部運行完畢整個程序終止
  2. 所有線程可能出現的最大等待時間 timeout_total ≤ timeout * 線程數量
  3. 雖然timeout設置的是5s,但是線程1只需要2s,所以循環從開始到結束,只需消耗2s(21:14:27 - 21:14:25),到此循環就進入第二次,第二次等待仍可以分配5s(21:14:32 - 21:14:27),所以兩次總共的等待是時間2+5=7s,但是線程2運行所需要時間是8s,而且8s是從21:14:25開始的,結束時間是21:14:33,因為join等待時間完了主程序結束了,但不影響線程2繼續運行,所以在end join後,線程2仍然輸出了報道結果(是因為沒有開啟守護線程)
  4. 個別文章解釋join這個時間為:主線程會等待多個線程的timeout累加和,這個說法不准確,由3的推理可以得出,並非會一定等待“線程數* timeout”這麼多時間,而是≤“線程數*timeout”,

第五種:加入join方法設置同步(守護線程,join設置超時)

超時且未處理完畢的子線程將被直接終止(符合守護線程的特性)

import threading, time
def doWaiting1():
print('start waiting1: ' + time.strftime('%H:%M:%S') + "\n")
time.sleep(2)
print("線程1奉命報道")
print('stop waiting1: ' + time.strftime('%H:%M:%S') + "\n")
def doWaiting2():
print('start waiting2: ' + time.strftime('%H:%M:%S') + "\n")
time.sleep(8)
print("線程2奉命報道")
print('stop waiting2: ', time.strftime('%H:%M:%S') + "\n")
tsk = []
# 創建並開啟線程1
thread1 = threading.Thread(target = doWaiting1)
thread1.setDaemon(True) # 守護線程
thread1.start()
tsk.append(thread1)
# 創建並開啟線程2
thread2 = threading.Thread(target = doWaiting2)
thread2.setDaemon(True) # 守護線程
thread2.start()
tsk.append(thread2)
print('start join: ' + time.strftime('%H:%M:%S') )
for t in tsk:
print("開始:"+time.strftime('%H:%M:%S'))
print('%s線程到了'%t)
t.join(5)
print("結束:" + time.strftime('%H:%M:%S'))
print('end join: ' + time.strftime('%H:%M:%S') )

運行結果:

start waiting1: 21:24:14
start waiting2: 21:24:14
start join: 21:24:14
開始:21:24:14
<Thread(Thread-1, started daemon 9060)>線程到了
線程1奉命報道
stop waiting1: 21:24:16
結束:21:24:16
開始:21:24:16
<Thread(Thread-2, started daemon 13912)>線程到了
結束:21:24:21
end join: 21:24:21

結論

  1. 相比第四種,超時後主線程運行到end join則結束了,子線程2已經被終止停止運行

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved