今天來學習線程同步與併發,咱們先來看一下線程之間的幾種通訊方式:html
Event
:事件;編程
Critical Section
:臨界區;安全
Semaphone
:信號量;網絡
咱們先來看看下面的實例:多線程
from threading import Thread, Event import time def teacher(event: Event): print('I am teacher , waiting for your homework') event.wait() print("I am teacher, already obtaining student's homework ") def student(event: Event): finished_homework = [] while len(finished_homework) < 10: time.sleep(1) print('I am student, finished one homework') finished_homework.append(1) else: print('student finish homework') event.set() if __name__ == '__main__': event = Event() Thread(target=student, args=(event,)).start() Thread(target=teacher, args=(event,)).start()
這個實例首先從threading 模塊中導入了Thread和Event兩個類,而後定義了兩個方法teacher和student,在if __name__ == '__main__'
中執行了這兩個方法。併發
按照正常通常的代碼執行順序,會先打印teacher方法中的第一個print,而後來到了event.wait()
,wait()
方法的語法爲wait(self, timeout=None)
,timeout用於設置等待的時長,若是超過期長則再也不等待,直接向下執行,若是timeout沒有指定則一 直等待,等待的時候是阻塞的;app
咱們能夠看到teacher方法中的event.wait()
並無設置timeout,因此會阻塞,開始執行student方法,student方法裏面是一個while循環,須要打印10遍print中的內容,打印完畢後進入else,其中有一個event.set()
方法,會將flag設置爲True,wait等待的線程就能夠向下執行;函數
因此剛剛teacher方法中等待的會繼續執行,咱們來看一下輸出結果:學習
I am teacher , waiting for your homework I am student, finished one homework I am student, finished one homework I am student, finished one homework I am student, finished one homework I am student, finished one homework I am student, finished one homework I am student, finished one homework I am student, finished one homework I am student, finished one homework I am student, finished one homework student finish homework I am teacher, already obtaining student's homework
event實例對象的對象方法介紹:fetch
wait(self, timeout=None):timeout
爲設置等待的時長,若是超過期長(返回值爲False)則再也不等待,直接向下執行,若是timeout沒有指定則一 直等待,等待的時候是阻塞的沒有返回值,;
set()
:若是執行event.set(),將會設置flag爲True,那麼wait等待的線程就能夠向下執行;
clear()
:若是執行event.clear(),將會設置flag標記爲Flase, 那麼wait等待的線程將再次等待(阻塞);
is_set()
:判斷event的flag是否爲True,若是爲True的話wait等待的線程將向下執行;
thread.allocate_lock() threading.Lock()
鎖是解決臨界區資源的問題,保證每個線程訪問臨界資源的時候有所有的權利,一旦某個線程得到鎖, 其它試圖獲取鎖的線程將被阻塞;
locked()方法
:用於判斷當前是否上鎖,若是上鎖,返回True,不然返回False;
acquire(blocking=True,timeout=-1)
:表示加鎖,默認True爲加鎖狀態(阻塞),False爲不阻塞,timeout爲設置時長;
release()方法
:用於釋放鎖,在完成任務的時候釋放鎖,讓其餘的線程獲取到臨界資源,注意此時必須處於上鎖的狀態,若是試圖釋放一個unlocked 的鎖,將拋出異常 thread.error
;
咱們經過兩個實例來看一下線程鎖的做用:
實例1: import time from threading import Thread homework_list = [] def student(number): while len(homework_list) < number: # 知足條件則繼續執行下面的代碼 time.sleep(0.001) # 等待0.001秒 homework_list.append(1) print(len(homework_list)) if __name__ == '__main__': for i in range(10): Thread(target=student, args=(10, )).start() # 同時開啓10個線程 time.sleep(3) # 等待3秒 print('完成做業的總數爲: {}'.format(len(homework_list)))
運行實例1,咱們發現最終輸出結果完成做業的總數爲:19,可是根據代碼邏輯來看,應該輸出10纔對,這是爲何呢?實際上是由於在多線程狀況下操做臨界資源,出現了臨界資源爭搶的問題,那如何解決這個問題呢,咱們來看實例2;
實例2: import time import threading from threading import Thread, Lock homework_list = [] lock = Lock() # 全局阻塞鎖 def student(number): while True: lock.acquire() # 必定要在獲取臨界資源以前加鎖 if len(homework_list) >= number: # 知足條件則跳出循環,不知足則繼續 break time.sleep(0.001) # 等待0.001秒 homework_list.append(1) lock.release() # 完成任務的時候釋放鎖,讓其餘的線程獲取到臨界資源 print('current_thread={}, homework_list={}'.format(threading.current_thread().name, len(homework_list))) if __name__ == '__main__': for i in range(10): Thread(target=student, name='student {}'.format(i), args=(10, )).start() # 同時開啓10個線程 time.sleep(3) # 等待3秒 print('完成做業的總數爲: {}'.format(len(homework_list)))
運行實例2發現最終輸出完成做業的總數爲: 10,是咱們想要的結果,實例1和實例2 不一樣之處在於實例2在獲取臨界資源以前加了鎖,也就是lock.acquire()
,咱們知道一旦某個線程得到鎖, 其它試圖獲取鎖的線程將被阻塞,因此這裏不會再發生臨界資源爭搶的問題了;
with lock
,會默認自動釋放鎖,不須要再使用release()方法來釋放鎖了,這樣能夠避免程序中忘記釋放鎖,更加方便;import time import threading from threading import Thread, Lock homework_list = [] # 全局阻塞鎖 lock = Lock() def student(number): while True: with lock: if len(homework_list) >= number: break time.sleep(0.001) homework_list.append(1) print('current_thread={}, homework_list={}'.format(threading.current_thread().name, len(homework_list))) if __name__ == '__main__': for i in range(10): Thread(target=student, name='student {}'.format(i), args=(1000, )).start() time.sleep(3) print('完成做業的總數爲: {}'.format(len(homework_list)))
因此其實這樣寫,和上面的實例2是同樣的效果喲,而且省略了釋放鎖的步驟,由於with默認自動釋放鎖,這樣就不怕忘記釋放鎖而致使代碼報錯了;
接下來學習一下線程池,也就是ThreadPoolExecutor
,線程池在構造實例的時候,會傳入max_workers參數來設置線程池中最多能同時運行的線程數目;
線程池實例對象有兩個很是使用的方法,submit方法和map方法:
submit(self, fn, *args, **kwargs)
:提交線程須要執行的任務(函數名和參數)到線程池中,並返回該任務的句柄,用於提交單個任務;
map(self, fn, *iterables, timeout=None, chunksize=1)
:相似高階函數map,能夠提交任務,且傳遞一個可迭代對象,返回任務處理迭代對象的結果;
from concurrent.futures import ThreadPoolExecutor import requests def fetch_url(url): result = requests.get(url=url, ) return result.text # 建立10個線程隊列的線程池 pool = ThreadPoolExecutor(10) # 獲取任務返回對象 a = pool.submit(fetch_url, 'http://www.baidu.com') # 取出返回的結果 x = a.result() print(x) # 得到百度的源碼
儘管Python徹底支持多線程編程, 可是解釋器的C語言實現部分在徹底並行執行時並非線程安全的,實際上,解釋器被一個全局解釋器鎖保護着 ,它確保任什麼時候候都只有一個Python線程執行;
GIL最大的問題就是Python的多線程程序並不能利用多核CPU的優點, 就是由於GIL的存在,使得一個進程的多個線程在執行任務的時候,一個CPU時 間片內,只有一個線程可以調度到CPU上運行;
所以CPU密集型的程序通常不會使用Python實現,能夠選擇Java,GO等語言;
可是對於非CPU密集型程序,例如IO密集型程序,多數時間都是對網絡IO的等待,所以Python的多線程徹底能夠勝任;
對於全局解釋器鎖的解決方案:
使用multiprocessing建立進程池對象,實現多進程併發,這樣就可以使用多CPU計算資源;
使用C語言擴展,將計算密集型任務轉移給C語言實現去處理,在C代碼實現部分能夠釋放GIL;
多線程和多進程解決方案: