Python 之 多任務:html
Python之多任務是如今多任務編程運用Python語言爲載體的一種體現。其中涵蓋:進程、線程、併發等方面的內容,以及包括近些年在大數據運算、人工智能領域運用強大的GPU運算能力實現的各類算法。屬於Python語言中比較高級的應用形式。文章採起問答的形式對知識點和相關應用的模式進行詳解,看似像意識流形態的文章,其內容也是遵循按部就班,力求全面和精細。算法
問:什麼是多任務?編程
答:百度定義:「當多任務處理是指用戶能夠在同一時間內運行多個應用程序,每一個應用程序被稱做一個任務。」也就是說操做系統能夠同時運行多個任務。這個概念是基於操做系統來講的。好比電腦同時開啓多個軟件或者應用程序,這就是一種多任務的體現;於此相對若是咱們電腦只能每次開啓一個軟件或者一個應用程序,關閉前者才能開啓後者這就叫作單任務,在早期的電腦基本都屬於單任務,對於如今的計算機應用基本上都屬於多任務的形式存在,所以咱們也只討論多任務。windows
問:CPU是整個運算的核心,CPU、操做系統、多任務之間的關係什麼?緩存
答:1. 如今的CPU基本上都是2核以上的CPU,若是咱們如今手裏有一臺單核的CPU和一臺多核CPU,咱們的多任務在上面又是怎麼實現的呢?安全
1.1. 單核CPU實現多任務原理?多線程
操做系統輪流讓各個任務交替執行,CPU對每一個任務進行「時間切片」的動做,這個切片時間很是短暫,打個比方,一整塊的奶酪難如下嚥,咱們把奶酪切成一小塊一小塊的,讓咱們的CPU以飛快的速度進行「吞噬」。一樣咱們好比有N個任務,把每個任務切成「時間切片」,如此交替往復的切換運行。這樣表面上看像是多任務一塊兒執行,可是CPU調度執行速度太快了,致使咱們感受全部任務都是同事執行同樣。架構
1.2. 多核CPU實現多任務原理?併發
真正的並行執行多任務只能在多核CPU上實現,可是任務數量遠遠多於CPU核心數量,因此操做系統會把不少任務輪流調度到每一個核心上執行。和單核CPU實現多任務原理相比,多核CPU真正實現了「多窗口」的輪流調度任務。app
1.3. 打個比方,單核CPU就像銀行開一個窗口來處理任務,多核CPU開多個窗口來處理任務。這個任務之間的調度是經過操做系統來完成的。
2. CPU核和線程是一個什麼概念?
這裏再稍微擴展一下:咱們說一下CPU的主頻、核心、線程、架構。
CPU是整個計算機的大腦。
主頻:咱們常常看到CPU參數當中有3.0GHz、3.7GHz等,這就是CPU的主頻,嚴謹的說他是CPU的時鐘頻率,也能夠直接理解爲運算速度。主頻越大力量越大。
核心:咱們經常說計算機是幾核心的。主頻咱們能夠理解爲肌肉,核心咱們能夠理解爲胳膊,若是你有16條胳膊(也就是16核心),你所能幹的事情也是越多。直觀來看在CPU中心隆起的芯片就是核心,是由單晶硅以必定的生產工藝製造出來的。每個CPU核心都有本身固定的邏輯結構,一級緩存、二級緩存,執行單元、指令集單元和總線接口等。打個好比,一條胳膊都有肌肉,骨骼、神經、血管等一套邏輯系統。
線程:有了胳膊還必須有手才能工做。通常來講單核配單線程,雙核配雙線程。也就說一條胳膊配一隻手。可是咱們有些CPU好比4核8線程,也就是說有4條胳膊,每條胳膊配2隻手(看起來有點兒恐怖了),可是這樣多造出來的兩隻手,幹活的效率就大大的提升了。
架構:如今有了肌肉、胳膊、手,就差一個工具就能夠幹活了,這就是CPU的架構,架構對刑恩該影響巨大。好比如今通行的ARM架構等。
3. 關係:
計算機經過操做系統,把多個任務調度到CPU的核心上進行計算,他們之間是載體和對象,實現和被實現、調度和被調度之間的關係。
問:並行和併發是什麼概念?
答:併發:看上去是一塊兒執行,任務多於CPU核心數。
並行:真正一塊兒執行,任務數小於CPU核心數。
再直觀的理解一下:併發(Concurrent):指兩個或多個事件在同一時間間隔內發生,即交替作不一樣事的能力。並行(Parallel):指兩個或多個事件在同一時刻發生,即同時作不一樣的能力。咱們以進程舉例:併發就像是咱們前面說的單核CPU執行多任務的概念,交替執行,並行就像多核CPU執行多任務的概念,同時執行。再好比,咱們如今有一個4核4線程的CPU,咱們有5個任務一塊兒發送給CPU,這個時候後多是某幾個程序並行一塊兒運行,其餘多於的1個任務交替併發執行。若是咱們如今只有4個任務,這樣4個任務就能夠徹底「吃飽」了CPU的進程,就能夠並行(固然這裏是不考慮其餘因素的狀況下)。
問:進程、線程、任務都是什麼東西?
答:進程:對於操做系統而言,一個任務就是一個進程。進程是系統中程序執行和資源分配的基本單位。每一個進程都有本身的數據段、代碼段、堆棧段。
線程:在一個進程的內部,要同時幹多件事情(就是手的問題),就須要同時運行多個子任務,咱們把進程內的這些子任務叫作線程。也就是說線程是進程的子任務。
線程一般叫作情景進程。線程是經過向內側控件的併發執行的多任務。每一個線程都共享一個進程的資源。線程是最小的執行單元,而進程至少由一個線程組成。如何調度進程和線程,徹底是由操做系統決定,用戶程序不能本身決定何時執行。執行多長時間。
咱們閉上眼睛形象的比喻一下,在線程這根柱子上面,子任務不斷圍繞這根柱子交替來執行(這是符合咱們以前的單任務CPU的工做原理的),而後多根柱子又去並行多少個進程一塊兒來執行。從實際運行的過程來看,任務在線程這個最小單元是併發執行的,在任務數小於CPU核心數的狀況下,有可能仍是並行執行的。併發是線程的常常運做模式,並行是整體的實際運行調度,微觀來講任務都是在併發執行在線程上面的。
問:「搶佔式」和協同式任務模式?
答:在windows3.x的時代,那個時候的程序運行任務基本上是依靠代碼的編寫邏輯進行的。當一個程序運行完畢後再運行下一個程序,這樣作最大的風險就是若是一個程序出問題卡死就很容易出現死機的狀況。搶佔式任務模式,是如今基本的任務模式,好比如今咱們開啓了N個程序,如今咱們在打字,這個優先級最高,咱們就把它先推送給CPU讓它先去執行,若是咱們如今再打開一個程序,以前的打字的那個程序優先級退下,讓當前的程序推送到最高的優先級。咱們其實能夠經過Windows的任務管理器去觀察全部的當前任務。咱們發現這些任務並非按照必定有規律的順序排列進行執行的,若是當前程序優先級很高它就是排在前面,並且咱們若是靜止不動去觀察,他們的排序是會產生不斷變化的。這就是「搶佔式」任務模式。所以咱們的程序在進程或者線程當中去運行的話,也是這種搶佔式任務運行模式。
問:GIL在Python中的做用?
答:GIL全稱:global interpreter lock,全局編譯器鎖。
GIL是實現Python解釋器(CPython)時所引入的概念。Python中一個線程對應於C語言中的一個線程。Python爲了簡單,前期會在咱們的解釋器上面加一把很是大的鎖。GIL一次只有一個線程運行在CPU上執行咱們的字節碼。字節碼咱們能夠用dis這個包進行反編譯,得到字節碼。沒法將多個線程映射到多個CPU上,沒法體現多核的優點。若是用Java或者C語言咱們能夠將多個線程映射到多個CPU上造成併發或並行。PyPy編碼是去GIL話的,咱們大多數用測CPython解釋器是由GIL概念的。
咱們寫一個函數而後經過dis這個包獲取這個函數的字節碼:
import dis def add(a): a = a + 1 return a print(dis.dis(add))
字節碼翻譯以下:
咱們再來看看Python的官方文檔:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once.
This lock is necessary mainly because CPython’s memory management is not thread-safe.
(However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
在 CPython中,全局解釋器鎖或者說GIL是一個"Mutex"(互斥鎖),這個互斥鎖一次阻止來自執行Python字節碼的多個天然線程。這個鎖最主要的必須行是由於CPython的內存管理不是線程安全的。
因爲物理上的限制,各個CPU廠家在覈心頻率上的比賽已經被多核所替代,一切單個CPU爲了增大主頻提高「肌肉」硬度,但如今更像是用多核CPU去併發執行程序。Python開始支持多線程數據之間的完整性和代碼狀態的同步性,天然是要進行加鎖控制的,正如上面說的是線程安全的,無需在實現時考慮額外的內存鎖或同步操做。這一把全局排他鎖,無疑會給多線程的執行效率有不小影響。甚至幾乎等於單個線程的程序。GIL是站在線程的角度來講明這個問題的。
問:前面說的,因爲GIL這個全局互斥鎖讓線程是安全的,一個進程運行在一個CPU上面,是否是就不用考慮線程的同步呢?
答:事實上並非的!緣由很簡單,GIL是會被釋放的,釋放和加載的過程正是咱們要考慮的同步問題!
咱們來寫一段經典的加減過程的相互對衝的函數:
import threading total = 0 def add(): global total for i in range(1000000): total += 1 def desc(): global total for i in range(1000000): total -= 1 thread1 = threading.Thread(target=add) thread2 = threading.Thread(target=desc) thread1.start() thread2.start() thread1.join() thread2.join() print(total) # 第一遍結果:-262850 # 第二遍結果:-306734
咱們發現運行的結果兩次都不同。這就證實了GIL是被釋放的。也證實了GIL並非等待第一個線程完了以後再執行第二個線程,線程被任務徹底的佔有。他是在某個時刻適當的進行釋放的。這個是結合了咱們字節碼的執行了多少行數,好比字節碼執行了100行,或者1000行後他會釋放。這側面的說明了不是第一個函數加到了100萬次,而後再執行第二個函數減到100萬次的。
所以GIL釋放通常會在三種狀況釋放:
第一種:就是前面說的按照字節碼的行數進行釋放。
第二種:按照時間片來進行釋放。
第三種:若是遇到IO操做的時候會進行釋放。
若是:#1.dosomthing
#2.IO操做
#3.dosomthing
像這樣的操做的時候會主動釋放,從這個角度來講GIL並非限制的那麼死。
問:線程、進行、協程等待?從那個地方進行入手呢?
答:先從線程開始入手,先講解threading模塊。在Python中還有一個_thread模塊,這是更加底層的線程模塊,threading模式是對_thread更加高級的一種封裝。在早期的CPU是沒有線程的這個概念的,只有進程的概念,可是這樣對CPU的開銷過大,後期引入了線程的概念。咱們ctrl-alt-del鍵,就會在咱們的任務管理器當中看到如今正在執行或者排隊的線程(前面見過如今的任務模式是一種搶佔式任務模式)。
對於IO操做來講:多線程和多進程差異不大。對於操做系統而言,線程的調度比進行的調度室更加輕量級的。
咱們以一個簡單的IO例子來講明threading模塊的應用,由於正如前面說的三種釋放狀況,遇到IO操做的時候前面線程會釋放,IO操做結束後,後面的線程再跟進來。
代碼以下:
import time import threading def get_detail_html(url): print("get detail html started") time.sleep(2) print("get detail html end") def get_detail_url(url): print("get detail url started") time.sleep(2) print("get detail url end") if __name__ == '__main__': thread1 = threading.Thread(target=get_detail_html,args=("",)) thread2 = threading.Thread(target=get_detail_url,args=("",)) start_time = time.time() thread1.start() thread2.start() print("last time {}".format(time.time()-start_time))
咱們分析這個打印結果發現一個有意思的現象:
問:爲何前面代碼沒有執行完畢直接把時間給打印出來了?
答:這是由於在你執行thread1和thread2的時候,在背後隱藏了一個主線程。主線程不會等到兩個子線程結束了以後才把結果打印出來。所以咱們有以下這樣一個框架:
主線程----------------------- 併發執行
子線程 -- 併發執行
子線程 -- 併發執行
若是咱們不加限制的話,只要程序一運行,有沒有子線程,主線程都會併發執行,固然這裏有兩個子線,也就是說程序在一開始運行的時候,主線+子線程1+子線程2併發執行。
問:若是咱們想有這樣一種狀態:在程序運行的時候,咱們可不能夠控制主線程和子線程的運行前後關係呢?
答:答案是確定的,咱們引入「守護」和「阻塞」兩個概念。
好比:咱們讓程序併發後,其中線程1一直去執行,無論主線程和子線程2結束不結束,這就叫作守護,就好像打仗同樣,有人掩護大家都撤退了,我再撤退(其實他本身撤退不了)
仍是這段代碼,咱們在thread1.setDaemon方法,這隻爲True,進行線程守護(備註還能夠在Thread裏面,在args=後面加入,daemon=True其效果同樣的)。
if __name__ == '__main__': thread1 = threading.Thread(target=get_detail_html,args=("",)) thread2 = threading.Thread(target=get_detail_url,args=("",)) thread1.setDaemon(True) # thread2.setDaemon(True) start_time = time.time() thread1.start() thread2.start() print("last time {}".format(time.time()-start_time))
運行結果:
get detail html started get detail url started last time 0.0 ???????????? get detail url end
thread1去哪兒了?還在那裏進行守護(活着沒活着不知道)。這就是守護的概念。這個線程能夠一直運行而不阻塞主程序活着其餘子程序的退出。若是一個服務不能很容易的終端線程,或者即便讓線程工做到一半時終止也不會形成數據損失或破壞。這個守護線程是很是有用的。
再好比:若是前面有一個守護線程或者沒有守護線程,咱們在子線程中橫叉一槓子,對某一個子線程進行攔截(這裏用阻塞的詞語),直到徹底撤離了再執行到主程序的結束。這就用到了join的方法。
好比阻塞1個
thread1 = threading.Thread(target=get_detail_html,args=("",)) thread2 = threading.Thread(target=get_detail_url,args=("",)) # thread1.setDaemon(True) # thread2.setDaemon(True) start_time = time.time() thread1.start() thread2.start() thread1.join()
打印結果:
get detail html started get detail url started get detail url end get detail html end last time 4.006964206695557
好比阻塞2個:而且換一下位置
thread1 = threading.Thread(target=get_detail_html,args=("",)) thread2 = threading.Thread(target=get_detail_url,args=("",)) # thread1.setDaemon(True) # thread2.setDaemon(True) start_time = time.time() thread1.start()
thread1.join()
thread2.start()
thread2.join()
打印結果:
get detail html started get detail html end get detail url started get detail url end last time 6.0321221351623535
在好比同時守護和阻塞:
thread1 = threading.Thread(target=get_detail_html,args=("",)) thread2 = threading.Thread(target=get_detail_url,args=("",)) thread1.setDaemon(True) thread2.setDaemon(True) start_time = time.time() thread1.start() thread1.join() thread2.start() thread2.join()
打印結果:
get detail html started get detail html end get detail url started get detail url end last time 6.02401328086853
讓咱們總結一下:阻塞無論阻塞了幾個,都會等到所有疏通後再打印主程序的結束;若是前面有守護線程,前面的守護線程仍然活着也要用join阻塞去拯救前面的阻塞線程。另外咱們還發現了,join裏面是能夠傳遞參數的這裏須要傳遞一個浮點數來控制阻塞的時間,咱們經過print來觀察。
thread1.start() a1 = time.time() thread1.join() a2 = time.time() print(a2 - a1)
這個阻塞時間若是不設定的話,是和前面的代碼中運行的睡眠時間是一致的。若是咱們設置阻塞時間爲0.1秒鐘
thread1.start() a1 = time.time() thread1.join(0.1) a2 = time.time() print(a2 - a1) get detail html started 0.10939979553222656 get detail html end
能夠看到是能夠控制的。
問:這樣看線程增長守護和阻塞是很是必要的,是規矩如今在併發時的前後關係。咱們有看到了線程的實例化方式和線程的啓動,還有沒有其餘的使用方式呢?
答:固然咱們還能夠經過類的繼承方式,重洗Thread中的run方法,效果是同樣的,這屬於一種內嵌的寫入方式。記住這裏是重寫的是Thread這個模塊類中的這個方法。
import time import threading class GetDetailHtml(threading.Thread): def __init__(self,name): super().__init__(name=name) def run(self): print("get detail html started") time.sleep(2) print("get detail html end") class GetDetailUrl(threading.Thread): def __init__(self,name): super().__init__(name=name) def run(self): print("get detail Url started") time.sleep(2) print("get detail Url end") if __name__ == '__main__': thread1 = GetDetailHtml("get_detail_html") thread2 = GetDetailHtml("get_detail_url") thread1.start() thread2.start() thread1.join() thread2.join() # get detail html started # get detail html started # get detail html end # get detail html end
咱們能夠看到和前面的編寫方式也是比較適用的,若是咱們動態的方式這種方式是比較適用的。另外咱們還可經過for循環來循環把任務載入線程,原理很簡單後面也會寫到
另外咱們還能夠經過getName方法打印出咱們當前線程運行那個任務的名稱:這個方式很簡單就不在舉例了。
問:前面說過了,讓兩段程序在多線程直接交替執行。那在交替執行的同時,兩段程序之間可不能夠相互交互數據呢?
答:這就叫作多線程之間的通訊。好比兩段程序,對應一個數據點,相互交替進行修改的過程就叫作多線程之間的通訊,換句話說就是兩段動做須要協做來完成一件事情。多線程之間的通訊經常使用的兩種方式:第一種:採起全局變量(共享變量)的方式;第二種:採起隊列的方式。
第一種:採用全局變量的方式:
import time import threading detail_url_list = [] def get_detail_html(): global detail_url_list url = detail_url_list.pop() print("get detail html started") time.sleep(2) print("get detail html end") def get_detail_url(): global detail_url_list print("get detail url started") time.sleep(2) for i in range(20): detail_url_list.append("http://projectsedu.com/{id}".format(id=i)) print("get detail url end") if __name__ == '__main__': thread_detail_url = threading.Thread(target=get_detail_url) for i in range(10): html_thread =threading.Thread(target=get_detail_html) html_thread.start() # thread_detail_html = threading.Thread(target=)
這種方式是可行的,但其實咱們更好的作法是把list這段數據端的數據單獨生成一個.py文件,專門用於存放數據有關的.py文件,而後經過import把它引用進來,這樣作的安全性會跟高。
import time import threading from chapter11 import variabls def get_detail_html(): detail_url_list = variabls.detail_url_list while True: if len(detail_url_list): url = detail_url_list.pop() print("get detail html started") time.sleep(2) print("get detail html end") def get_detail_url(): detail_url_list = variabls.detail_url_list while True: print("get detail url started") time.sleep(2) for i in range(20): detail_url_list.append("http://projectsedu.com/{id}".format(id=i)) print("get detail url end") if __name__ == '__main__': thread_detail_url = threading.Thread(target=get_detail_url) for i in range(10): html_thread =threading.Thread(target=get_detail_html) html_thread.start() # thread_detail_html = threading.Thread(target=)
第二種:採起隊列的方式(這裏的隊列是用的單端隊列,不是雙端隊列)
import time import threading from queue import Queue def get_detail_html(queue): while True: url = queue.get() # 這是一種阻塞的方法 print("get detail html started") time.sleep(2) print("get detail html end") def get_detail_url(queue): while True: print("get detail url started") time.sleep(2) for i in range(20): queue.put("http://projectsedu.com/{id}".format(id=i)) print("get detail url end") if __name__ == '__main__': detail_url_queue = Queue(maxsize=1000) thread_detail_url = threading.Thread(target=get_detail_url,args=(detail_url_queue,)) for i in range(10): html_thread =threading.Thread(target=get_detail_html,args=(detail_url_queue,)) html_thread.start() detail_url_queue.join() detail_url_queue.task_done()
咱們發現queue方法是線程安全的,獲取數據採用.get()方法,這個方法從原碼咱們能夠看到是一種阻塞的方式,只有當這個動做完成後,才能進行下面的方式。推入隊列的方式我麼用的是put方法。
另外,咱們在隊列當中也能夠實現相似於線程中的daemon守護和join阻塞的方式。對應的隊列裏面是task_done和join方法。固然這些也是須要成對出現的。
問:咱們想到了最先的加減問題,由於線程前面有一把GIL的大鎖,這把鎖並非排隊按照順序執行的,到必定的邏輯時會出現線程釋放的狀況致使了計算結果的誤差,咱們可不能夠控制線程在任務執行時的順序呢?
答:這就是線程同步的問題,分幾個部分來講:
第一部分:關於鎖的概念,Lock和RLock
from threading import Lock import threading total = 0 lock = Lock() def add(): global total,lock for i in range(1000000): lock.acquire() total += 1 lock.release() def desc(): global total,lock for i in range(1000000): lock.acquire() total -= 1 lock.release() thread1 = threading.Thread(target=add) thread2 = threading.Thread(target=desc) thread1.start() thread2.start() thread1.join() thread2.join() print(total) # 0
前面的代碼咱們經過線程鎖限制了鎖中內容的計算,致使最後結果統一。
不過咱們發現加入了鎖以後會影響性能,鎖會引發死鎖。因此咱們要注意以下這麼幾個問題:
第一種問題:
for i in range(1000000): lock.acquire() lock.acquire() total += 1 lock.release()
像這種咱們獲取了鎖,而不釋放就會形成死鎖
第二種問題:
def add(): global total,lock for i in range(1000000): lock.acquire() dosomething(lock) total += 1 lock.release() def dosomething(lock): lock.acquire() # do something lock.release()
咱們發現若是咱們再在鎖中間再加一把鎖,這樣作就會產生錯誤:鎖中鎖是不容許的!可是Python給咱們提供了一種可重入的鎖RLock就能夠解決這個問題。
可重入的鎖RLock:
在同一個線程裏面,能夠連續調用屢次acquire,可是咱們必定要注意acquire的次數和release必定要配對。這樣咱們從新改一下代碼,發現不會再報錯。
from threading import Lock,RLock import threading total = 0 lock = Lock() rlock = RLock() def add(): global total,lock,rlock for i in range(1000000): lock.acquire() dosomething(rlock) total += 1 lock.release() def dosomething(rlock): rlock.acquire() # do something rlock.release() def desc(): global total,lock for i in range(1000000): lock.acquire() total -= 1 lock.release() thread1 = threading.Thread(target=add) thread2 = threading.Thread(target=desc) thread1.start() thread2.start() thread1.join() thread2.join() print(total)
第二部分:Condition條件變量,用於實現複雜的線程同步
前面咱們看到了,第一段函數執行完了,而後釋放了,再執行第二段片斷。可是咱們若是有這麼一種狀況,咱們讓兩我的來對古詩,你一句我一句。用上面的方法是否還可行呢?
按照這個邏輯咱們寫一下代碼:以天貓和小愛來進行對話:
import threading from threading import Lock class XiaoAi(threading.Thread): def __init__(self,lock): super().__init__(name="小愛") self.lock = lock def run(self): lock.acquire() print("{}:{}".format(self.name, "在")) lock.release() lock.acquire() print("{}:{}".format(self.name, "好啊")) lock.release() class TianMao(threading.Thread): def __init__(self,lock): super().__init__(name="天貓精靈") self.lock = lock def run(self): lock.acquire() print("{}:{}".format(self.name,"小愛同窗")) lock.release() lock.acquire() print("{}:{}".format(self.name,"咱們在對古詩吧?")) lock.release() if __name__ == '__main__': lock = Lock() tianmao = TianMao(lock) xiaoai = XiaoAi(lock) tianmao.start() xiaoai.start()
咱們來查看返回的結果:
天貓精靈:小愛同窗
天貓精靈:咱們在對古詩吧?
小愛:在
小愛:好啊
並非我一言你一語的那種對話了吧。這是由於用鎖的概念,只能按照順序來解鎖。這就致使了不能交替來執行,所以咱們要用條件來解決這個問題。也就是說某一個條件結束了再來解釋其餘條件,而不能用鎖的概念。所以咱們須要的是某一種狀態的鎖定和解鎖,而不是像鎖這樣簡單的順序開鎖解鎖。
所以在使用條件的話咱們要知道的不是acquire和release,而須要的是wait和notify,wait是等待某一個條件變量的通知,notify是通知調用某一個waiti的部分去通知他們啓動,咱們更改代碼以下:(注意咱們用condition的時候用的是with語句,這樣可以在退出程序的時候天然釋放condition狀態。)
import threading from threading import Condition class XiaoAi(threading.Thread): def __init__(self,cond): super().__init__(name="小愛") self.cond = cond def run(self): with self.cond: self.cond.wait() print("{}:{}".format(self.name, "在")) self.cond.notify() self.cond.wait() print("{}:{}".format(self.name, "好啊")) class TianMao(threading.Thread): def __init__(self,cond): super().__init__(name="天貓精靈") self.cond = cond def run(self): with self.cond: print("{}:{}".format(self.name,"小愛同窗")) self.cond.notify() self.cond.wait() print("{}:{}".format(self.name, "咱們對古詩吧?")) self.cond.notify() if __name__ == '__main__': cond = threading.Condition() tianmao = TianMao(cond) xiaoai = XiaoAi(cond) xiaoai.start() tianmao.start()
咱們查看輸出:
天貓精靈:小愛同窗
小愛:在
天貓精靈:咱們對古詩吧?
小愛:好啊
這樣就實現了咱們的對話,可是這裏有兩點注意:
第一點要使用with語句,若是咱們不用with語句的話,先後要加上cond.acquire()和cond.release(),咱們採用with語句是很是方便的方式,能夠不用考慮釋放的問題。固然前面的lock也能夠用with語句。所以看with語句是很是方便的。
第二點咱們要把小愛先啓動,目的是爲了等待天貓給他的說話。