python進階(9)多線程
什麼是線程?
線程也叫輕量級進程
,是操作系統能夠進行運算調度
的最小
單位,它被包涵在進程之中,是進程中的實際運作單位。線程自己不擁有系統資源
,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其他線程共享進程所擁有的全部資源。一個線程可以創建和撤銷另一個線程,同一個進程中的多個線程之間可以並發
執行
為什麼要使用多線程?
線程在程序中是獨立的
、並發的
執行流。與分隔的進程相比,進程中線程之間的隔離程度要小,它們共享內存、文件句柄 和其他進程應有的狀態。 因為線程的劃分尺度小於進程,使得多線程程序的並發性高
。進程在執行過程之中擁有獨立的內存單元,而多個線程共享
內存,從而極大的提升了程序的運行效率
。 線程比進程具有更高的性能,這是由於同一個進程中的線程都有共性,多個線程共享一個進程的虛擬空間。線程的共享環境包括進程代碼段、進程的共有數據等,利用這些共享的數據,線程之間很容易實現通信。 操作系統在創建進程時,必須為進程分配獨立的內存空間,並分配大量的相關資源,但創建線程則簡單得多。因此,使用多線程來實現並發
比使用多進程的性能高得要多。
多線程優點
進程之間不能共享內存,但線程之間共享內存非常容易。操作系統在創建進程時,需要為該進程重新分配系統資源,但創建線程的代價則小得多。因此使用多線程來實現多任務並發執行
比使用多進程的效率高 python語言內置了多線程功能支持,而不是單純地作為底層操作系統的調度方式,從而簡化了python的多線程編程。
單線程執行
import time
def hello():
print("你好,世界")
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
hello()
運行結果
多線程執行
import threading
import time
def saySorry():
print("你好,世界")
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=saySorry) # 創建線程對象,此時還未啟動子線程
t.start() # 啟動線程,即讓線程開始執行
運行結果
執行速度對比
- 可以明顯看出使用了多線程並發的操作,花費時間要短
- 當調用start()時,才會真正的創建線程,並且開始執行
函數式創建多線程
python中多線程使用threading
模塊,threading模塊調用Thread類
self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None
- group:默認為None;預留給將來擴展ThreadGroup時使用類實現。不常用,可以忽略
- target:代表要執行的函數名,不是函數
- name:線程名,默認情況下的格式是”Thread-N”,其中N是一個小的十進制數
- args:函數的參數,以元組的形式表示
- kwargs:關鍵字參數字典
小例子
import threading
from time import sleep
from datetime import datetime
def write(name):
for i in range(3):
print("{}正在寫字{}".format(name, i))
sleep(1)
def draw(name):
for i in range(3):
print("{}正在畫畫{}".format(name, i))
sleep(1)
if __name__ == '__main__':
print(f'---開始---:{datetime.now()}')
t1 = threading.Thread(target=write, args=('Jack', ))
t2 = threading.Thread(target=draw, args=('Tom', ))
t1.start()
t2.start()
print(f'---結束---:{datetime.now()}')
查看線程數量threading.enumerate()
import threading
from datetime import datetime
from time import sleep
def write():
for i in range(3):
print(f"正在寫字...{i}")
sleep(1)
def draw():
for i in range(3):
print(f"正在畫畫...{i}")
sleep(1)
if __name__ == '__main__':
print(f'---開始---:{datetime.now()}')
t1 = threading.Thread(target=write)
t2 = threading.Thread(target=draw)
t1.start()
t2.start()
while True:
length = len(threading.enumerate())
print(f'當前運行的線程數為:{length}')
if length <= 1:
break
sleep(0.5)
結果
最開始打印線程數為3個,一個主線程+2個子線程t1,t2
最後打印線程數為1個,是因為子線程都結束了,就剩主線程了
自定義線程
繼承threading.Thread
來定義線程類,其本質是重構Thread
類中的run
方法
為什麼執行run方法,就會啟動線程呢?之前寫函數時,調用的是start()
方法
因為run方法里默認執行了start()
方法
import threading
from time import sleep
class MyThread(threading.Thread):
def run(self):
for i in range(5):
sleep(1)
msg = "I'm " + self.name + ' @ ' + str(i) # name屬性中保存的是當前線程的名字
print(msg)
if __name__ == '__main__':
t = MyThread()
t.start()
結果
守護線程
'''
這裡使用setDaemon(True)把所有的子線程都變成了主線程的守護線程,
因此當主線程結束後,子線程也會隨之結束,所以當主線程結束後,整個程序就退出了。
所謂』線程守護』,就是主線程不管該線程的執行情況,只要是其他子線程結束且主線程執行完畢,主線程都會關閉。也就是說:主線程不等待該守護線程的執行完再去關閉。
'''
import threading
import time
def run(n):
print('task', n)
time.sleep(1)
print('3s')
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
if __name__ == '__main__':
t = threading.Thread(target=run, args=('t1',))
t.setDaemon(True)
t.start()
print('end')
結果
task t1
end
通過執行結果可以看出,設置守護線程之後,當主線程結束時,子線程也將立即結束,不再執行
主線程等待子線程結束(join)
為了讓守護線程執行結束之後,主線程再結束,我們可以使用join
方法,讓主線程等待子線程執行
import threading
import time
def run(n):
print('task', n)
time.sleep(1)
print('3s')
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
if __name__ == '__main__':
t = threading.Thread(target=run, args=('t1',))
t.setDaemon(True) # 把子線程設置為守護線程,必須在start()之前設置
t.start()
t.join() # 設置主線程等待子線程結束
print('end')
結果
task t1
3s
2s
1s
end
線程共享變量
'''
多線程共享全局變量
線程時進程的執行單元,進程時系統分配資源的最小執行單位,所以在同一個進程中的多線程是共享資源的
'''
import threading
import time
g_num = 0
def work1(num):
global g_num
for i in range(num):
g_num += 1
print("----in work1, g_num is %d---"%g_num)
def work2(num):
global g_num
for i in range(num):
g_num += 1
print("----in work2, g_num is %d---"%g_num)
print("---線程創建之前g_num is %d---"%g_num)
t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()
t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2個線程對同一個全局變量操作之後的最終結果是:%s" % g_num)
結果
---線程創建之前g_num is 0---
----in work2, g_num is 1451293---
----in work1, g_num is 1428085---
2個線程對同一個全局變量操作之後的最終結果是:1428085
先來看結果,為什麼不是200000呢?
原因是多線程共用同一個變量,可能會出現資源競爭的問題,導致數據不準確,那有什麼解決辦法嗎?下面介紹互斥鎖
互斥鎖
由於線程之間是進行隨機調度,並且每個線程可能只執行n條執行之後,當多個線程同時修改同一條數據時可能會出現臟數據
,所以出現了線程鎖
,即同一時刻允許一個線程執行操作。線程鎖用於鎖定資源,可以定義多個鎖,像下面的代碼,當需要獨佔 某一個資源時,任何一個鎖都可以鎖定這個資源,就好比你用不同的鎖都可以把這個相同的門鎖住一樣。
由於線程之間是進行隨機調度
的,如果有多個線程同時操作一個對象,如果沒有很好地保護該對象,會造成程序結果的不可預期,我們因此也稱為線程不安全
。
為了防止上面情況的發生,就出現了互斥鎖(Lock)
import threading
import time
g_num = 0
# 創建一個互斥鎖
# 默認是未上鎖的狀態
lock = threading.Lock()
def test1(num):
global g_num
for i in range(num):
lock.acquire() # 上鎖
g_num += 1
lock.release() # 解鎖
print("---test1---g_num=%d"%g_num)
def test2(num):
global g_num
for i in range(num):
lock.acquire() # 上鎖
g_num += 1
lock.release() # 解鎖
print("---test2---g_num=%d"%g_num)
# 創建2個線程,讓他們各自對g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()
p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()
# 等待計算完成
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2個線程對同一個全局變量操作之後的最終結果是:%s" % g_num)
結果
---test2---g_num=1961182
---test1---g_num=2000000
2個線程對同一個全局變量操作之後的最終結果是:2000000
上鎖解鎖過程
當一個線程調用鎖的acquire()
方法獲得鎖時,鎖就進入locked
狀態。 每次只有一個線程可以獲得鎖。如果此時另一個線程試圖獲得這個鎖,該線程就會變為blocked
狀態,稱為「阻塞」,直到擁有鎖的線程調用鎖的release()
方法釋放鎖之後,鎖進入unlocked
狀態。 線程調度程序從處於同步阻塞狀態的線程中選擇一個來獲得鎖,並使得該線程進入運行(running)狀態。
鎖的好處
- 確保了某段關鍵代碼只能由一個線程從頭到尾完整地執行
鎖的壞處
- 阻止了多線程
並發
執行,包含鎖的某段代碼實際上只能以單線程模式
執行,效率就大大地下降了 - 由於可以存在多個鎖,不同的線程持有不同的鎖,並試圖獲取對方持有的鎖時,可能會造成
死鎖
GIL全局解釋器
在非python環境中,單核情況下,同時只能有一個任務執行。多核時可以支持多個線程同時執行
。但是在python中,無論有多少個核同時只能執行一個線程。究其原因,這就是由於GIL
的存在導致的。
GIL的全程是全局解釋器
,來源是python設計之初的考慮,為了數據安全所做的決定。某個線程想要執行,必須先拿到GIL,我們可以把GIL看做是「通行證
」,並且在一個python進程之中,GIL只有一個。拿不到線程的通行證,並且在一個python進程中,GIL只有一個,拿不到通行證的線程,就不允許進入CPU執行。GIL只在cpython
中才有,因為cpython調用的是c語言的原生線程
,所以他不能直接操作cpu,而只能利用GIL保證同一時間
只能有一個線程拿到數據。而在pypy
和jpython
中是沒有GIL的
python在使用多線程的時候,調用的是c語言的原生過程
。
python針對不同類型的代碼執行效率也是不同的
CPU密集型代碼
(各種循環處理、計算等),在這種情況下,由於計算工作多,ticks技術很快就會達到閥值
,然後出發GIL的 釋放與再競爭(多個線程來回切換當然是需要消耗資源的),所以python下的多線程對CPU密集型
代碼並不友好
。
IO密集型代碼
(文件處理、網絡爬蟲等設計文件讀寫操作),多線程能夠有效提升效率
(單線程下有IO操作會進行IO等待, 造成不必要的時間浪費,而開啟多線程能在線程A等待時,自動切換到線程B,可以不浪費CPU的資源,從而能提升程序的執行 效率)。所以python的多線程對IO密集型
代碼比較友好
。
主要要看任務的類型,我們把任務分為I/O密集型
和計算密集型
,而多線程在切換中又分為I/O切換
和時間切換
。如果任務屬於是I/O密集型
,若不採用多線程,我們在進行I/O操作時,勢必要等待前面一個I/O任務完成後面的I/O任務才能進行,在這個等待的過程中,CPU處於等待狀態
,這時如果採用多線程的話,剛好可以切換到進行另一個I/O任務。這樣就剛好可以充分利用CPU避免CPU處於閑置狀態
,提高效率。但是,如果多線程任務都是計算型
,CPU會一直在進行工作,直到一定的時間後採取多線程時間切換的方式進行切換線程,此時CPU一直處於工作狀態, 此種情況下並不能提高性能
,相反在切換多線程任務時,可能還會造成時間
和資源的浪費
,導致效能下降。這就是造成上面兩種多線程結果不能的解釋。
結論:I/O密集型
任務,建議採取多線程,還可以採用多進程
+協程
的方式(例如:爬蟲多採用多線程處理爬取的數據);對於計算密集型
任務,python此時就不適用了。