同步
同步的概念
同步就是協同步調,按預定的先后次序進行運行。如:你說完,我再說。"同"字從字面上容易理解為一起動作,其實不是,"同"字應是指協同、協助、互相配合。如進程、線程同步,可理解為進程或線程A和B一塊配合,A執行到一定程度時要依靠B的某個結果,於是停下來,示意B運行;B執行,再將結果給A;A再繼續操作。
解決線程同時修改全局變量的方式
對於上一篇中(多線程共享全局變量)的那個計算錯誤的問題,可以通過線程同步來進行解決。
思路,如下:
- 系統調用t1,然后獲取到g_num的值為0,此時上一把鎖,即不允許其他線程操作g_num
- t1對g_num的值進行+1
- t1解鎖,此時g_num的值為1,其他的線程就可以使用g_num了,而且是g_num的值不是0而是1
- 同理其他線程在對g_num進行修改時,都要先上鎖,處理完后再解鎖,在上鎖的整個過程中不允許其他線程訪問,就保證了數據的正確性
互斥鎖
當多個線程幾乎同時修改某一個共享數據的時候,需要進行同步控制。線程同步能夠保證多個線程安全訪問競爭資源,最簡單的同步機制是引入互斥鎖。
互斥鎖為資源引入一個狀態:鎖定/非鎖定
某個線程要更改共享數據時,先將其鎖定,此時資源的狀態為“鎖定”,其他線程不能更改;直到該線程釋放資源,將資源的狀態變成“非鎖定”,其他的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操作,從而保證了多線程情況下數據的正確性。
threading模塊中定義了Lock類,可以方便的處理鎖定:
# 創建鎖
mutex = threading.Lock()
# 鎖定
mutex.acquire()
# 釋放
mutex.release()
注意:
- 如果這個鎖之前是沒有上鎖的,那么acquire不會堵塞
- 如果在調用acquire對這個鎖上鎖之前 它已經被 其他線程上了鎖,那么此時acquire會堵塞,直到這個鎖被解鎖為止
使用互斥鎖完成2個線程對同一個全局變量各加9999999 次的操作
import threading
import time
g_num = 0
# 創建一個互斥鎖,默認為未上鎖zhuangtai
mutex = threading.Lock()
def test1(num):
global g_num
for i in range(num):
mutex.acquire()
g_num += 1
mutex.release()
print("--test1, g_num = %d--" % g_num)
def test2(num):
global g_num
for i in range(num):
mutex.acquire()
g_num += 1
mutex.release()
print("--test2, g_num = %d--" % g_num)
if __name__ == "__main__":
print("--創建線程之前, g_num = %d--" % g_num)
t1 = threading.Thread(target=test1, args=(9999999,))
t1.start()
t2 = threading.Thread(target=test2, args=(9999999,))
t2.start()
while len(threading.enumerate()) != 1:
time.sleep(1)
print("最終結果為:g_num=%d" % g_num)
運行結果:
--創建線程之前, g_num = 0--
--test1, g_num = 19580251--
--test2, g_num = 19999998--
最終結果為:g_num=19999998
可以看到最后的結果,加入互斥鎖后,其結果與預期相符。
上鎖解鎖過程
當一個線程調用鎖的acquire()方法獲得鎖時,鎖就進入“locked”狀態。
每次只有一個線程可以獲得鎖。如果此時另一個線程試圖獲得這個鎖,該線程就會變為“blocked”狀態,稱為“阻塞”,直到擁有鎖的線程調用鎖的release()方法釋放鎖之后,鎖進入“unlocked”狀態。
線程調度程序從處於同步阻塞狀態的線程中選擇一個來獲得鎖,並使得該線程進入運行(running)狀態。
總結
鎖的好處:
- 確保了某段關鍵代碼只能由一個線程從頭到尾完整地執行
鎖的壞處:
- 阻止了多線程並發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地下降了
- 由於可以存在多個鎖,不同的線程持有不同的鎖,並試圖獲取對方持有的鎖時,可能會造成死鎖
死鎖
在線程間共享多個資源的時候,如果兩個線程分別占有一部分資源並且同時等待對方的資源,就會造成死鎖。
盡管死鎖很少發生,但一旦發生就會造成應用的停止響應。下面看一個死鎖的例子
import threading
import time
class MyThread1(threading.Thread):
def run(self):
# 對mutex1上鎖
mutex1.acquire()
# mutex1上鎖后,延遲1秒,等待另外一個線程,把mutex2上鎖
print(self.name + " up")
time.sleep(1)
# 此時這里會堵塞,因為mutex2已經被另外的線程搶先上鎖了
mutex2.acquire()
print(self.name + " down")
mutex2.release()
# 對mutex1解鎖
mutex1.release()
class MyThread2(threading.Thread):
def run(self):
# 對mutex2上鎖
mutex2.acquire()
# mutex2上鎖后,延遲1秒,等待另外一個線程,把mutex1上鎖
print(self.name + " up")
time.sleep(1)
# 此時這里會堵塞,因為mutex1已經被另外的線程搶先上鎖了
mutex1.acquire()
print(self.name + " down")
mutex1.release()
# 對mutex2解鎖
mutex2.release()
mutex1 = threading.Lock()
mutex2 = threading.Lock()
if __name__ == "__main__":
t1 = MyThread1()
t1.start()
t2 = MyThread2()
t2.start()
運行結果:
Thread-1 up
Thread-2 up
此時已經進入到了死鎖狀態,可以使用ctrl-c退出
避免死鎖
- 程序設計時要盡量避免(銀行家算法)
- 添加超時時間等