多線程:
什么是多線程:
- 理解:默認情況下,一個程序只有一個進程和一個線程,代碼是依次線性執行的。而多線程則可以並發執行,一次性多個人做多件事,自然比單線程更快。
- 官方:https://baike.baidu.com/item/多線程/1190404?fr=aladdin
如何創建一個基本的多線程:
使用threading模塊下的Thread類即可創建一個線程。這個類有一個target參數,需要指定一個函數,那么以后這個線程執行的時候,就會執行這個函數的代碼。示例代碼如下:
import time
import threading
def coding():
for x in range(3):
print("%d正在寫代碼..."%x)
time.sleep(1)
def drawing():
for x in range(3):
print("%d正在畫圖..." % x)
time.sleep(1)
def multi_thread():
th1 = threading.Thread(target=coding)
th2 = threading.Thread(target=drawing)
th1.start()
th2.start()
if __name__ == '__main__':
# single_thread()
multi_thread()
查看當前線程:
- threading.current_thread:在線程中執行這個函數,會返回當前線程的對象。
- threading.enumerate:獲取整個程序中所有的線程。
繼承自threading.Thread類:
- 我們自己寫的類必須繼承自
threading.Thread類。 - 線程代碼需要放在run方法中執行。
- 以后創建線程的時候,直接使用我們自己創建的類來創建線程就可以了。
- 為什么要使用類的方式創建線程呢?原因是因為類可以更加方便的管理我們的代碼,可以讓我們使用面向對象的方式進行編程。
全局變量共享的問題:
在多線程中,如果需要修改全局變量,那么需要在修改全局變量的地方使用鎖鎖起來,執行完成后再把鎖釋放掉。
使用鎖的原則:
- 把盡量少的和不耗時的代碼放到鎖中執行。
- 代碼執行完成后要記得釋放鎖。
在Python中,可以使用threading.Lock來創建鎖,lock.acquire()是上鎖操作,lock.release()是釋放鎖的操作。
生產者和消費者模式:
生產者和消費者模式是多線程開發中經常見到的一種模式。生產者的線程專門用來生產一些數據,然后存放到一個中間的變量中。消費者再從這個中間的變量中取出數據進行消費。通過生產者和消費者模式,可以讓代碼達到高內聚低耦合的目標,程序分工更加明確,線程更加方便管理。
Lock版本的生產者和消費者模式:
import threading
import random
gMoney = 0
gLock = threading.Lock()
gTimes = 0
class Producer(threading.Thread):
def run(self) -> None:
global gMoney
global gTimes
while True:
gLock.acquire()
if gTimes >= 10:
gLock.release()
break
money = random.randint(0, 100)
gMoney += money
gTimes += 1
print("%s生產了%d元錢"%(threading.current_thread().name,money))
gLock.release()
class Consumer(threading.Thread):
def run(self) -> None:
global gMoney
while True:
gLock.acquire()
money = random.randint(0,100)
if gMoney >= money:
gMoney -= money
print("%s消費了%d元錢"%(threading.current_thread().name,money))
else:
if gTimes >= 10:
gLock.release()
break
print("%s想消費%d元錢,但是余額只有%d"%(threading.current_thread().name,money,gMoney))
gLock.release()
def main():
for x in range(5):
th = Producer(name="生產者%d號"%x)
th.start()
for x in range(5):
th = Consumer(name="消費者%d號"%x)
th.start()
if __name__ == '__main__':
main()
Condition版本的生產者和消費者模式:
Lock版本的生產者與消費者模式可以正常的運行。但是存在一個不足,在消費者中,總是通過while True死循環並且上鎖的方式去判斷錢夠不夠。上鎖是一個很耗費CPU資源的行為。因此這種方式不是最好的。還有一種更好的方式便是使用threading.Condition來實現。threading.Condition可以在沒有數據的時候處於阻塞等待狀態。一旦有合適的數據了,還可以使用notify相關的函數來通知其他處於等待狀態的線程。這樣就可以不用做一些無用的上鎖和解鎖的操作。可以提高程序的性能。首先對threading.Condition相關的函數做個介紹,threading.Condition類似threading.Lock,可以在修改全局數據的時候進行上鎖,也可以在修改完畢后進行解鎖。以下將一些常用的函數做個簡單的介紹:
- acquire:上鎖。
- release:解鎖。
- wait:將當前線程處於等待狀態,並且會釋放鎖。可以被其他線程使用notify和notify_all函數喚醒。被喚醒后會繼續等待上鎖,上鎖后繼續執行下面的代碼。
- notify:通知某個正在等待的線程,默認是第1個等待的線程。
- notify_all:通知所有正在等待的線程。notify和notify_all不會釋放鎖。並且需要在release之前調用。
代碼如下:
import threading
import random
import time
gMoney = 0
gCondition = threading.Condition()
gTimes = 0
class Producer(threading.Thread):
def run(self) -> None:
global gMoney
global gTimes
while True:
gCondition.acquire()
if gTimes >= 10:
gCondition.release()
break
money = random.randint(0, 100)
gMoney += money
gTimes += 1
print("%s生產了%d元錢,剩余%d元錢"%(threading.current_thread().name,money,gMoney))
gCondition.notify_all()
gCondition.release()
time.sleep(1)
class Consumer(threading.Thread):
def run(self) -> None:
global gMoney
while True:
gCondition.acquire()
money = random.randint(0,100)
while gMoney < money:
if gTimes >= 10:
print("%s想消費%d元錢,但是余額只有%d元錢了,並且生產者已經不再生產了!"%(threading.current_thread().name,money,gMoney))
gCondition.release()
return
print("%s想消費%d元錢,但是余額只有%d元錢了,消費失敗!"%(threading.current_thread().name,money,gMoney))
gCondition.wait()
gMoney -= money
print("%s消費了%d元錢,剩余%d元錢"%(threading.current_thread().name,money,gMoney))
gCondition.release()
time.sleep(1)
def main():
for x in range(5):
th = Producer(name="生產者%d號"%x)
th.start()
for x in range(5):
th = Consumer(name="消費者%d號"%x)
th.start()
if __name__ == '__main__':
main()
線程安全的隊列Queue:
在線程中,訪問一些全局變量,加鎖是一個經常的過程。如果你是想把一些數據存儲到某個隊列中,那么Python內置了一個線程安全的模塊叫做queue模塊。Python中的queue模塊中提供了同步的、線程安全的隊列類,包括FIFO(先進先出)隊列Queue,LIFO(后入先出)隊列LifoQueue。這些隊列都實現了鎖原語(可以理解為原子操作,即要么不做,要么都做完),能夠在多線程中直接使用。可以使用隊列來實現線程間的同步。相關的函數如下:
初始化Queue(maxsize):創建一個先進先出的隊列。
- qsize():返回隊列的大小。
- empty():判斷隊列是否為空。
- full():判斷隊列是否滿了。
- get():從隊列中取最后一個數據。默認情況下是阻塞的,也就是說如果隊列已經空了,那么再調用就會一直阻塞,直到有新的數據添加進來。也可以使用
block=False,來關掉阻塞。如果關掉了阻塞,在隊列為空的情況獲取就會拋出異常。 - put():將一個數據放到隊列中。跟get一樣,在隊列滿了的時候也會一直阻塞,並且也可以通過block=False來關掉阻塞,同樣也會拋出異常。
GIL:
什么是GIL:
Python自帶的解釋器是CPython。CPython解釋器的多線程實際上是一個假的多線程(在多核CPU中,只能利用一核,不能利用多核)。同一時刻只有一個線程在執行,為了保證同一時刻只有一個線程在執行,在CPython解釋器中有一個東西叫做GIL(Global Intepreter Lock),叫做全局解釋器鎖。這個解釋器鎖是有必要的。因為CPython解釋器的內存管理不是線程安全的。當然除了CPython解釋器,還有其他的解釋器,有些解釋器是沒有GIL鎖的,見下面:
- Jython:用Java實現的Python解釋器。不存在GIL鎖。更多詳情請見:https://zh.wikipedia.org/wiki/Jython
- IronPython:用.net實現的Python解釋器。不存在GIL鎖。更多詳情請見:https://zh.wikipedia.org/wiki/IronPython
- PyPy:用Python實現的Python解釋器。存在GIL鎖。更多詳情請見:https://zh.wikipedia.org/wiki/PyPy
GIL雖然是一個假的多線程。但是在處理一些IO操作(比如文件讀寫和網絡請求)還是可以在很大程度上提高效率的。在IO操作上建議使用多線程提高效率。在一些CPU計算操作上不建議使用多線程,而建議使用多進程。
有了GIL,為什么還需要Lock:
GIL只是保證全局同一時刻只有一個線程在執行,但是他並不能保證執行代碼的原子性。也就是說一個操作可能會被分成幾個部分完成,這樣就會導致數據有問題。所以需要使用Lock來保證操作的原子性。
