1、共享全局變量資源競爭
前面文章介紹了一個線程寫入,一個線程讀取,沒問題,
但是如果兩個線程都寫入會造成阻塞
import threading
num = 0
def demo1(nums):
global num
for i in range(nums):
num += 1
print('demo1--------%d' % num) # demo1--------1171003 數值異常就是資源競爭的結果
def demo2(nums):
global num
for i in range(nums):
num += 1
print('demo2--------%d' % num) # demo2--------1262790 數值異常就是資源競爭的結果
def main():
t1 = threading.Thread(target=demo1, args=(1000000, ))
t2 = threading.Thread(target=demo2, args=(1000000, ))
t1.start()
t2.start()
print('main-------%d' % num) # main-------541178
if __name__ == '__main__':
main()
圖中的結果在理論上不對的原因是:
在執行demo1的子線程的過程中,還沒有完全執行完成,就會被CPU執行demo2的子線程,當demo2的子線程尚未執行完成的時候,優惠回來執行demo1的子線程剩餘的任務,最後繼續執行demo2,所以有可能會造成資源競爭,數值顯示異常,這是一個概率性的問題,可能發生,可能不會發生。
上圖中的案例就是說明了python執行代碼的方式:
1、先加載a參數;load a
2、加載1;load 1
3、執行add函數:inplace_add
4、賦值給a
2、互斥鎖
當多個線程幾乎同時修改某一個共享數據的時候,需要進行同步控制
某個線程要更改共享數據時,先將其鎖定,此時資源的狀態爲"鎖定",其他線程不能改變,只到該線程釋放資源,將資源的狀態變成"非鎖定",其他的線程才能再次鎖定該資源。
互斥鎖保證了每次只有一個線程進行寫入操作,從而保證了多線程情況下數據的正確性。
創建鎖
mutex = threading.Lock()
鎖定
mutex.acquire()
解鎖
mutex.release()
time.sleep():爲了解決主線程會在子線程運行的時候,還沒結束就調用裏面的參數,輸出了異常值
import threading
import time
num = 0
# 創建一個互斥鎖,默認是沒有上鎖的
mutex = threading.Lock()
def demo1(nums):
global num
# 加鎖
mutex.acquire()
for i in range(nums):
num += 1
# 解鎖
mutex.release()
print('demo1--------%d' % num)
def demo2(nums):
global num
# 加鎖
mutex.acquire()
for i in range(nums):
num += 1
# 解鎖
mutex.release()
print('demo2--------%d' % num)
def main():
t1 = threading.Thread(target=demo1, args=(1000000,))
t2 = threading.Thread(target=demo2, args=(1000000,))
t1.start()
t2.start()
time.sleep(2) # 這裏加上延遲2s 爲了解決主線程會在子線程運行的時候,還沒結束就調用裏面的參數,輸出了異常值
print('main-------%d' % num)
if __name__ == '__main__':
main()
'''
demo1--------1000000
demo2--------2000000
main-------2000000
'''
3、死鎖
在線程間共享多個資源的時候,如果兩個線程分別佔有一部分資源並且同時等待對方的資源,就會造成死鎖。
import threading
import time
class MyThread1(threading.Thread):
def run(self):
# 對mutexA上鎖
mutexA.acquire()
# mutexA上鎖後,延時1秒,等待另外那個線程 把mutexB上鎖
print(self.name+'----do1---up----')
time.sleep(1)
# 此時會堵塞,因爲這個mutexB已經被另外的線程搶先上鎖了
mutexB.acquire()
print(self.name+'----do1---down----')
mutexB.release()
# 對mutexA解鎖
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
# 對mutexB上鎖
mutexB.acquire()
# mutexB上鎖後,延時1秒,等待另外那個線程 把mutexA上鎖
print(self.name+'----do2---up----')
time.sleep(1)
# 此時會堵塞,因爲這個mutexA已經被另外的線程搶先上鎖了
mutexA.acquire()
print(self.name+'----do2---down----')
mutexA.release()
# 對mutexB解鎖
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
可重入的鎖
mutex = threading.RLock()
可重入的鎖 可以重複加多把鎖 但是加鎖和解鎖數目要一樣
import threading
import time
num = 0
# 創建一個互斥鎖,默認是沒有上鎖的
# mutex = threading.Lock()
# 可重入的鎖 可以重複加多把鎖 但是加鎖和解鎖數目要一樣
mutex = threading.RLock()
def demo1(nums):
global num
# 加鎖
mutex.acquire()
mutex.acquire() # 多個加鎖
for i in range(nums):
num += 1
# 解鎖
mutex.release()
mutex.release() # 對應多個解鎖
print('demo1--------%d' % num)
def demo2(nums):
global num
# 加鎖
mutex.acquire()
for i in range(nums):
num += 1
# 解鎖
mutex.release()
print('demo2--------%d' % num)
def main():
t1 = threading.Thread(target=demo1, args=(1000000,))
t2 = threading.Thread(target=demo2, args=(1000000,))
t1.start()
t2.start()
time.sleep(2) # 這裏加上延遲2s 爲了解決主線程會在子線程運行的時候,還沒結束就調用裏面的參數,輸出了異常值
print('main-------%d' % num)
if __name__ == '__main__':
main()
'''
demo1--------1000000
demo2--------2000000
main-------2000000
'''
避免死鎖—銀行家算法
- 程序設計時要儘量避免 (採用不同步執行的設計思路)
- 添加超時時間等限制條件
銀行家算法介紹:
假設有三個客戶C1,C2,C3,向銀行家借款,該銀行家的資金總額爲10個資金單位,其中C1客戶要借9各資金單位,C2客戶要借3個資金單位,C3客戶要借8個資金單位,總計20個資金單位。如何操作能夠滿足所有客戶的需求?
操作流程:
1、先借款給C1客戶2個資金單位,剩餘7個資金單位再分批次給;
2、再借款給C2客戶2個資金單位,剩餘1個資金單位再分批次給;
3、再借款給C3客戶4個資金單位,剩餘4個資金單位再分批次給;
4、將銀行剩餘的2個資金單位,先借給C2客戶1個資金單位,C2客戶所有借款完成,並回收所有借出的3個資金單位,銀行剩餘資金爲:4個資金單位+C2客戶返還的利息;
5、將銀行剩餘的4個資金單位,借款給C3客戶,完成C3客戶的所有借款,然後收回本金的8個資金單位+利息;
6、將銀行剩餘的8個資金單位,借款給C1客戶,完成C3客戶的所有借款,然後收回本金的9個資金單位+利息;
算法完成,銀行最後得到10個資金單位+C1\C2\C3客戶返還的利息。
4、線程同步
"""線程同步
天貓精靈:小愛同學
小愛同學:在
天貓精靈:現在幾點了?
小愛同學:你猜猜現在幾點了
"""
import threading
class XiaoAi(threading.Thread):
def __init__(self, cond):
super().__init__(name='小愛')
# self.lock = lock
self.cond = cond
def run(self):
with self.cond: # 使用with語句因爲Condition()的源碼中的__enter__和__exit__方法
print("4")
# 等待接受notify()
self.cond.wait() # 根據第一句執行完的notify(),這裏是等待
print("{}:在".format(self.name))
print("5")
# 通知
self.cond.notify() # 執行完,發出通知,讓後面執行語句接收到
print("6")
# 等待接受notify()
self.cond.wait()
print("{}:你猜猜現在幾點了".format(self.name))
print("8")
# 通知
self.cond.notify()
class TianMao(threading.Thread):
def __init__(self, cond):
super().__init__(name='天貓精靈')
# self.lock = lock
self.cond = cond
def run(self):
# 加鎖
self.cond.acquire() # 與上面with語句不同的表達方式
print("{}:小愛同學".format(self.name))
print("1")
# 通知
self.cond.notify() # 執行完第一句,發出通知,讓後面執行語句接收到
print("2")
# 等待接受notify()
self.cond.wait()
print("3")
print("{}:現在幾點了?".format(self.name))
print("7")
# 通知
self.cond.notify()
# 解鎖
self.cond.release() # 對應的解鎖
if __name__ == '__main__':
# mutex = threading.RLock()
cond = threading.Condition()
xiaoai = XiaoAi(cond)
tianmao = TianMao(cond)
xiaoai.start() # 啓動順序很重要 否則功能實現不了
tianmao.start()
上面代碼需要注意的是:
– 啓動順序很重要 否則功能實現不了
– 加鎖和解鎖數目一定要相等
– 邏輯順序和代碼執行順序不同,下面會介紹
下圖是:功能實現的邏輯順序
下圖是:代碼實際執行順序
5、多任務版udp聊天
1 創建套接字
2 綁定本地信息
3 獲取對方IP和端口
4 發送、接收數據
5 創建兩個線程,去執行功能
import socket
import threading
def recv_data(udp_socket):
while True:
recv_data = udp_socket.recvfrom(1024)
print(recv_data)
def send_data(udp_socket, dest_ip, dest_port):
# 發送數據
while True:
send_data = input("請輸入要發送的數據:")
# udp_socket.sendto(send_data.encode('gbk'), ("192.168.0.111", 7777))
udp_socket.sendto(send_data.encode('gbk'), (dest_ip, dest_port))
def main():
"""完成udp聊天器"""
# 創建套接字
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# 綁定
udp_socket.bind(("", 7777))
# 獲取對方ip和端口
dest_ip = input("請輸入對方的ip:")
dest_port = int(input("請輸入對方的端口:"))
# 上面代碼用線程思想完成
t_recv_data = threading.Thread(target=recv_data, args=(udp_socket, )) # 元組類型數據,不需要對方ip端口
t_send_data = threading.Thread(target=send_data, args=(udp_socket, dest_ip, dest_port))
t_recv_data.start()
t_send_data.start()
if __name__ == '__main__':
main()
如下圖所示:需要ip和端口相匹配,注意圖中對應的值。