一多任務的引入
有很多的場景中的事情是同時進行的,比如開車的時候手和腳共同來駕駛汽車,再比如唱歌跳舞也是同時進行的;
在編程中,一邊唱歌一邊跳舞是如何實現的呢?
·很顯然剛剛的程序并沒有完成唱歌和跳舞同時進行的要求
·如果想要實現“唱歌跳舞”同時進行,那么就需要一個新的方法,叫做:多任務
二多任務的概念
什么叫“多任務”呢?簡單地說,就是操作系統可以同時運行多個任務。打個比方,你一邊在用瀏覽器上網,一邊在聽MP3,一邊在用Word趕作業,這就是多任務,至少同時有3個任務正在運行。還有很多任務悄悄地在后臺同時運行著,只是桌面上沒有顯示而已。
現在,多核CPU已經非常普及了,但是,即使過去的單核CPU,也可以執行多任務。由于CPU執行代碼都是順序執行的,那么,單核CPU是怎么執行多任務的呢?
答案就是操作系統輪流讓各個任務交替執行,任務1執行0.01秒,切換到任務2,任務2執行0.01秒,再切換到任務3,執行0.01秒……這樣反復執行下去。表面上看,每個任務都是交替執行的,但是,由于CPU的執行速度實在是太快了,我們感覺就像所有任務都在同時執行一樣。
真正的并行執行多任務只能在多核CPU上實現,但是,由于任務數量遠遠多于CPU的核心數量,所以,操作系統也會自動把很多任務輪流調度到每個核心上執行。
三進程的創建-fork
1.進程 vs 程序
編寫完畢的代碼,在沒有運行的時候,稱之為程序
正在運行著的代碼,就成為進程
進程,除了包含代碼以外,還有需要運行的環境等,所以和程序是有區別的。
2.fork
Python的os模塊封裝了常見的系統調用,其中就包括fork,可以在Python程序中輕松創建子進程:
·程序執行到os.fork()時,操作系統會創建一個新的進程(子進程),然后復制父進程的所有信息到子進程中。
·然后父進程和子進程都會從fork()函數中得到一個返回值,在子進程中這個值一定是0,而父進程中是子進程的id號。
在Unix/Linux操作系統中,提供了一個fork()系統函數,它非常特殊。
普通的函數調用,調用一次,返回一次,但是fork()調用一次,返回兩次,因為操作系統自動把當前進程(稱為父進程)復制了一份(稱為子進程),然后,分別在父進程和子進程內返回。
子進程永遠返回0,而父進程返回子進程的ID。
這樣做的理由是,一個父進程可以fork出很多子進程,所以,父進程要記下每個子進程的ID,而子進程只需要調用getppid()就可以拿到父進程的ID。
四getpid(),getppid()
通過os.pid()和os.ppid()可以獲得程序的子進程的進程號和父進程的進程號
五多進程修改全局變量
·多進程中,每個進程中所有數據(包括全局變量)都各有擁有一份,互不影響。
六多次fork問題
如果在一個程序,有2次的fork函數調用,是否就會有3個進程呢?
父子進程的執行順序
父進程、子進程執行順序沒有規律,完全取決于操作系統的調度算法
七multiprocessing
1 fork只能在Unix/Linux上使用
multiprocessing模塊就是跨平臺版本的多進程模塊,可以在windows系統上使用。
multiprocessing模塊提供了一個Process類來代表一個進程對象,下面的例子演示了啟動一個子進程并等待其結束:
創建子進程時,只需要傳入一個執行函數和函數的參數,創建一個Process實例,用start方法啟動,比fork()還簡單.
·join()方法可以等待子進程結束后再繼續往下運行,通常用于進程間的同步。
Process語法結構如下:
Process([group [, target [, name [, args [, kwargs]]]]])
target:表示這個進程實例所調用對象
args:表示調用對象的位置參數元組
kwargs:表示調用對象的關鍵字參數字典
name:為當前進程實例的別名
group:大多數情況下用不到
Process類常用方法:
·is_alive():判斷進程實例是否還在執行;
·join([timeout]):是否等待進程實例執行結束,或等待多少秒;
·start():啟動進程實例(創建子進程);
·run():如果沒有給定target參數,對這個對象調用start()方法時,就將執行對象中的run()方法;
·terminate():不管任務是否完成,立即終止;
Process類常用屬性:
·name:當前進程實例別名,默認為Process-N,N為從1開始遞增的整數;
·pid:當前進程實例的PID值;
實例1:
實例二:
八進程的創建-Process子類
創建新的進程還能夠使用類的方式,可以自定義一個類,繼承Process類,每次實例化這個類的時候,就等同于實例化一個進程對象,請看下面的實例:
兩種方式的對比:
1、方法
2、繼承類
繼承類是以面向對象考慮這個事的,所以業務邏輯復雜,建議使用繼承類,更好理解
九進程池Pool
當需要創建的子進程數量不多時,可以直接利用multiprocessing中的Process動態成生多個進程,但如果是上百甚至上千個目標,手動的去創建進程的工作量巨大,此時就可以用到multiprocessing模塊提供的Pool方法。
初始化Pool時,可以指定一個最大進程數,當有新的請求提交到Pool中時,如果池還沒有滿,那么就會創建一個新的進程用來執行該請求;但如果池中的進程數已經達到指定的最大值,那么該請求就會等待,直到池中有進程結束,才會創建新的進程來執行,請看下面的實例:
multiprocessing.Pool常用函數解析:
1創建一個進程池
from multiprocessing import Pool
po=Pool(num)
num等于進程池中的進程數量,如果不設置num默認可以放任意數量進程
2為進程池中添加進程(非阻塞式)
po.apply_async(func,(args,),(kwargs,))
使用非阻塞方式調用func,func為創建子進程的函數名(并行執行,阻塞方式必須等待上一個進程退出才能執行下一個進程),args為傳遞給func的參數列表,kwargs為傳遞給func的關鍵字參數列表;
3為進程池中添加進程(阻塞式)
po.apply(func,(args,),(kwargs,))
和非阻塞式最大的區別就是上一個進程退出才能調用下一個進程
4關閉進程池
po.close()
關閉Pool,使其不再接受新的任務
5終止進程
pool.terminate()
不管任務是否完成,立刻終止
6 join
pool.join()
主進程阻塞,等待子進程的退出,必須在close或terminate之后使用
apply阻塞式的實例:
十進程間通信-Queue
Process之間有時需要通信,操作系統提供了很多機制來實現進程間的通信。
1 Queue的使用
可以使用multiprocessing模塊中的Queue來實現多進程之間的數據傳遞,Queue本身是一個消息列隊程序,下面用一個小程序來演示Queue的工作原理:
推薦放消息的方式:判斷隊列是否已滿,再寫入;
推薦讀消息的方式:判斷隊列是否為空,再讀?。?/p>
說明
初始化Queue()對象時(例如q=Queue()),若括號中沒有指定最大可接收的消息數量或數量為負值,那么就代表可接受的消息數量沒有上限。
q=Queue(3)
①q.qsize()
返回當前隊列中包含的消息數量;
②q.empty()
如果隊列為空,返回True,反之False;
③q.full()
·如果隊列滿了,返回True,反之False;
④q.get(block,timeout)
·獲取隊列中的一條消息,然后將其從列隊中移除,block默認值為True;
1)如果block使用默認值,且沒有設置timeout(單位秒),消息列隊如果為空,此時程序將被阻塞(停在讀取狀態),直到從消息列隊讀到消息為止,如果設置了timeout,則會等待timeout秒,若還沒讀取到任何消息,則拋出"Queue.Empty"異常;
2)如果block值為False,消息列隊如果為空,則會立刻拋出"Queue.Empty"異常;
⑤q.get_nowait()
相當Queue.get(False);
⑥q.put(item,[block[, timeout]])
將item消息寫入隊列,block默認值為True;
1)如果block使用默認值,且沒有設置timeout(單位秒),消息列隊如果已經沒有空間可寫入,此時程序將被阻塞(停在寫入狀態),直到從消息列隊騰出空間為止,如果設置了timeout,則會等待timeout秒,若還沒空間,則拋出"Queue.Full"異常;
2)如果block值為False,消息列隊如果沒有空間可寫入,則會立刻拋出"Queue.Full"異常;
⑦q.put_nowait(item)
相當Queue.put(item, False);
2 Queue實例
注意參數的傳遞
我們以Queue為例,在父進程中創建兩個子進程,一個往Queue里寫數據,一個從Queue里讀數據:
3 進程池中的Queue
如果要使用Pool創建進程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否則會得到一條如下的錯誤信息:
RuntimeError: Queue objects should only be shared between processes through inheritance.
下面的實例演示了進程池中的進程如何通信: