線程跟進程有些相似,有時被稱作輕量級的進程,但不同的是,所有的線程運行在同一個進程中,共享相同的運行壞境。
進程和線程都是實現多任務的一種方式,例如:在同一台計算機上能同時運行多個QQ(進程),一個QQ可以打開多個聊天窗口(線程)。
資源共享:進程不能共享資源,而線程共享所在進程的地址空間和其他資源,同時,線程有自己的棧和棧指針。
解決線程共享全局變量問題
通過互斥鎖(lock)解決數據不同步的問題
當多個線程都對某項數據進行修改時,需要進行同步操作,線程同步能夠保證多個線程安全的訪問競爭資源,其中最簡單的同步機制就是引入互斥鎖。互斥鎖為資源引入一個狀態,鎖定或非鎖定,當某個線程要改共享的數據時,先將其鎖定,其他進程不能進行修改,
直到該線程釋放資源,使用互斥鎖每次只能有一個線程寫入操作,從而保證了多線程的數據的正確性。
具體分為三步驟
第一創建對象
lock=threading.Lock()
第二 鎖定資源
lock.acquire([blocking])
第三 釋放資源
lock.release()
下面來看下資源共享的問題用lock與不用lock
import time, threading # 假定這是你的銀行存款: balance = 0 def change_it(n): # 先存后取,結果應該為0: global balance balance = balance + n balance = balance - n def run_thread(n): for i in range(100000): change_it(n) t1 = threading.Thread(target=run_thread, args=(5,)) t2 = threading.Thread(target=run_thread, args=(8,)) t1.start() t2.start() t1.join() t2.join() print(balance)
我們定義了一個共享變量balance
,初始值為0
,並且啟動兩個線程,先存后取,理論上結果應該為0
,但是,由於線程的調度是由操作系統決定的,當t1、t2交替執行時,只要循環次數足夠多,balance
的結果就不一定是0
了。
原因是因為高級語言的一條語句在CPU執行時是若干條語句,即使一個簡單的計算:
balance = balance + n
也分兩步:
- 計算
balance + n
,存入臨時變量中; - 將臨時變量的值賦給
balance
。
也就是可以看成:
x = balance + n balance = x
由於x是局部變量,兩個線程各自都有自己的x,當代碼正常執行時:
初始值 balance = 0 t1: x1 = balance + 5 # x1 = 0 + 5 = 5 t1: balance = x1 # balance = 5 t1: x1 = balance - 5 # x1 = 5 - 5 = 0 t1: balance = x1 # balance = 0 t2: x2 = balance + 8 # x2 = 0 + 8 = 8 t2: balance = x2 # balance = 8 t2: x2 = balance - 8 # x2 = 8 - 8 = 0 t2: balance = x2 # balance = 0 結果 balance = 0
但是t1和t2是交替運行的,如果操作系統以下面的順序執行t1、t2:
初始值 balance = 0 t1: x1 = balance + 5 # x1 = 0 + 5 = 5 t2: x2 = balance + 8 # x2 = 0 + 8 = 8 t2: balance = x2 # balance = 8 t1: balance = x1 # balance = 5 t1: x1 = balance - 5 # x1 = 5 - 5 = 0 t1: balance = x1 # balance = 0 t2: x2 = balance - 8 # x2 = 0 - 8 = -8 t2: balance = x2 # balance = -8 結果 balance = -8
究其原因,是因為修改balance
需要多條語句,而執行這幾條語句時,線程可能中斷,從而導致多個線程把同一個對象的內容改亂了。
兩個線程同時一存一取,就可能導致余額不對,你肯定不希望你的銀行存款莫名其妙地變成了負數,所以,我們必須確保一個線程在修改balance
的時候,別的線程一定不能改。
如果我們要確保balance
計算正確,就要給change_it()
上一把鎖,當某個線程開始執行change_it()
時,我們說,該線程因為獲得了鎖,因此其他線程不能同時執行change_it()
,只能等待,直到鎖被釋放后,獲得該鎖以后才能改。由於鎖只有一個,無論多少線程,同一時刻最多只有一個線程持有該鎖,所以,不會造成修改的沖突。創建一個鎖就是通過threading.Lock()
來實現:
balance = 0 lock = threading.Lock() def run_thread(n): for i in range(100000): # 先要獲取鎖: lock.acquire() try: # 放心地改吧: change_it(n) finally: # 改完了一定要釋放鎖: lock.release()
當多個線程同時執行lock.acquire()
時,只有一個線程能成功地獲取鎖,然后繼續執行代碼,其他線程就繼續等待直到獲得鎖為止。
獲得鎖的線程用完后一定要釋放鎖,否則那些苦苦等待鎖的線程將永遠等待下去,成為死線程。所以我們用try...finally
來確保鎖一定會被釋放。
鎖的好處就是確保了某段關鍵代碼只能由一個線程從頭到尾完整地執行,壞處當然也很多,首先是阻止了多線程並發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地下降了。其次,由於可以存在多個鎖,不同的線程持有不同的鎖,並試圖獲取對方持有的鎖時,可能會造成死鎖,導致多個線程全部掛起,既不能執行,也無法結束,只能靠操作系統強制終止。
多線程編程,模型復雜,容易發生沖突,必須用鎖加以隔離,同時,又要小心死鎖的發生。