1、多任務的概念
什么叫“多任務”呢?簡單地說,就是操作系統可以同時運行多個任務。打個比方,你一邊在用瀏覽器上網,一邊在聽MP3,一邊在用Word趕作業,這就是多任務,至少同時有3個任務正在運行。還有很多任務悄悄地在后臺同時運行著,只是桌面上沒有顯示而已。
現在,多核CPU已經非常普及了,但是,即使過去的單核CPU,也可以執行多任務。由于CPU執行代碼都是順序執行的,那么,單核CPU是怎么執行多任務的呢?
答案就是操作系統輪流讓各個任務交替執行,任務1執行0.01秒,切換到任務2,任務2執行0.01秒,再切換到任務3,執行0.01秒……這樣反復執行下去。表面上看,每個任務都是交替執行的,但是,由于CPU的執行速度實在是太快了,我們感覺就像所有任務都在同時執行一樣。
真正的并行執行多任務只能在多核CPU上實現,但是,由于任務數量遠遠多于CPU的核心數量,所以,操作系統也會自動把很多任務輪流調度到每個核心上執行。
2、進程的創建-fork
- 進程 VS 程序
編寫完畢的代碼,在沒有運行的時候,稱之為程序
正在運行著的代碼,就成為進程
進程,除了包含代碼以外,還有需要運行的環境等,所以和程序是有區別的 - fork( )
Python的os模塊封裝了常見的系統調用,其中就包括fork,可以在Python程序中輕松創建子進程:
import os
# 注意,fork函數,只在Unix/Linux/Mac上運行,windows不可以
pid = os.fork()
if pid == 0:
print('哈哈1')
else:
print('哈哈2')
程序執行到os.fork()時,操作系統會創建一個新的進程(子進程),然后復制父進程的所有信息到子進程中
然后父進程和子進程都會從fork()函數中得到一個返回值,在子進程中這個值一定是0,而父進程中是子進程的 id號
在Unix/Linux操作系統中,提供了一個fork()系統函數,它非常特殊。
普通的函數調用,調用一次,返回一次,但是fork()調用一次,返回兩次,因為操作系統自動把當前進程(稱為父進程)復制了一份(稱為子進程),然后,分別在父進程和子進程內返回。
子進程永遠返回0,而父進程返回子進程的ID。
這樣做的理由是,一個父進程可以fork出很多子進程,所以,父進程要記下每個子進程的ID,而子進程只需要調用getppid()就可以拿到父進程的ID。
- getpid()、getppid()
import os
rpid = os.fork()
if rpid<0:
print("fork調用失敗。")
elif rpid == 0:
print("我是子進程(%s),我的父進程是(%s)"%(os.getpid(),os.getppid()))
x+=1
else:
print("我是父進程(%s),我的子進程是(%s)"%(os.getpid(),rpid))
print("父子進程都可以執行這里的代碼")
運行結果:
我是父進程(19360),我的子進程是(19361)
父子進程都可以執行這里的代碼
我是子進程(19361),我的父進程是(19360)
父子進程都可以執行這里的代碼
3、多進程修改全局變量
#coding=utf-8
import os
import time
num = 0
# 注意,fork函數,只在Unix/Linux/Mac上運行,windows不可以
pid = os.fork()
if pid == 0:
num+=1
print('哈哈1---num=%d'%num)
else:
time.sleep(1)
num+=1
print('哈哈2---num=%d'%num)
5、進程的創建-multiprocessing如果你打算編寫多進程的服務程序,Unix/Linux無疑是正確的選擇。由于Windows沒有fork調用,難道在Windows上無法用Python編寫多進程的程序?
由于Python是跨平臺的,自然也應該提供一個跨平臺的多進程支持。multiprocessing模塊就是跨平臺版本的多進程模塊。
multiprocessing模塊提供了一個Process類來代表一個進程對象,下面的例子演示了啟動一個子進程并等待其結束:
#coding=utf-8
from multiprocessing import Process
import os
# 子進程要執行的代碼
def run_proc(name):
print('子進程運行中,name= %s ,pid=%d...' % (name, os.getpid()))
if __name__=='__main__':
print('父進程 %d.' % os.getpid())
p = Process(target=run_proc, args=('test',))
print('子進程將要執行')
p.start()
p.join()
print('子進程已結束')
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
from multiprocessing import Process
import os
from time import sleep
# 子進程要執行的代碼
def run_proc(name, age, **kwargs):
for i in range(10):
print('子進程運行中,name= %s,age=%d ,pid=%d...' % (name, age,os.getpid()))
print(kwargs)
sleep(0.5)
if __name__=='__main__':
print('父進程 %d.' % os.getpid())
p = Process(target=run_proc, args=('test',18), kwargs={"m":20})
print('子進程將要執行')
p.start()
sleep(1)
p.terminate()
p.join()
print('子進程已結束')
運行結果:
父進程 21378.
子進程將要執行
子進程運行中,name= test,age=18 ,pid=21379...
{'m': 20}
子進程運行中,name= test,age=18 ,pid=21379...
{'m': 20}
子進程已結束
實例2
#coding=utf-8
from multiprocessing import Process
import time
import os
#兩個子進程將會調用的兩個方法
def worker_1(interval):
print("worker_1,父進程(%s),當前進程(%s)"%(os.getppid(),os.getpid()))
t_start = time.time()
time.sleep(interval) #程序將會被掛起interval秒
t_end = time.time()
print("worker_1,執行時間為'%0.2f'秒"%(t_end - t_start))
def worker_2(interval):
print("worker_2,父進程(%s),當前進程(%s)"%(os.getppid(),os.getpid()))
t_start = time.time()
time.sleep(interval)
t_end = time.time()
print("worker_2,執行時間為'%0.2f'秒"%(t_end - t_start))
#輸出當前程序的ID
print("進程ID:%s"%os.getpid())
#創建兩個進程對象,target指向這個進程對象要執行的對象名稱,
#args后面的元組中,是要傳遞給worker_1方法的參數,
#因為worker_1方法就一個interval參數,這里傳遞一個整數2給它,
#如果不指定name參數,默認的進程對象名稱為Process-N,N為一個遞增的整數
p1=Process(target=worker_1,args=(2,))
p2=Process(target=worker_2,name=“lawang",args=(1,))
#使用"進程對象名稱.start()"來創建并執行一個子進程,
#這兩個進程對象在start后,就會分別去執行worker_1和worker_2方法中的內容
p1.start()
p2.start()
#同時父進程仍然往下執行,如果p2進程還在執行,將會返回True
print("p2.is_alive=%s"%p2.is_alive())
#輸出p1和p2進程的別名和pid
print("p1.name=%s"%p1.name)
print("p1.pid=%s"%p1.pid)
print("p2.name=%s"%p2.name)
print("p2.pid=%s"%p2.pid)
#join括號中不攜帶參數,表示父進程在這個位置要等待p1進程執行完成后,
#再繼續執行下面的語句,一般用于進程間的數據同步,如果不寫這一句,
#下面的is_alive判斷將會是True,在shell(cmd)里面調用這個程序時
#可以完整的看到這個過程,大家可以嘗試著將下面的這條語句改成p1.join(1),
#因為p2需要2秒以上才可能執行完成,父進程等待1秒很可能不能讓p1完全執行完成,
#所以下面的print會輸出True,即p1仍然在執行
p1.join()
print("p1.is_alive=%s"%p1.is_alive())
執行結果:
進程ID:19866
p2.is_alive=True
p1.name=Process-1
p1.pid=19867
p2.name=laowang
p2.pid=19868
worker_1,父進程(19866),當前進程(19867)
worker_2,父進程(19866),當前進程(19868)
worker_2,執行時間為'1.00'秒
worker_1,執行時間為'2.00'秒
p1.is_alive=False
7、進程池Pool
當需要創建的子進程數量不多時,可以直接利用multiprocessing中的Process動態成生多個進程,但如果是上百甚至上千個目標,手動的去創建進程的工作量巨大,此時就可以用到multiprocessing模塊提供的Pool方法。
初始化Pool時,可以指定一個最大進程數,當有新的請求提交到Pool中時,如果池還沒有滿,那么就會創建一個新的進程用來執行該請求;但如果池中的進程數已經達到指定的最大值,那么該請求就會等待,直到池中有進程結束,才會創建新的進程來執行,請看下面的實例:
from multiprocessing import Pool
import os,time,random
def worker(msg):
t_start = time.time()
print("%s開始執行,進程號為%d"%(msg,os.getpid()))
#random.random()隨機生成0~1之間的浮點數
time.sleep(random.random()*2)
t_stop = time.time()
print(msg,"執行完畢,耗時%0.2f"%(t_stop-t_start))
po=Pool(3) #定義一個進程池,最大進程數3
for i in range(0,10):
#Pool.apply_async(要調用的目標,(傳遞給目標的參數元祖,))
#每次循環將會用空閑出來的子進程去調用目標
po.apply_async(worker,(i,))
print("----start----")
po.close() #關閉進程池,關閉后po不再接收新的請求
po.join() #等待po中所有子進程執行完成,必須放在close語句之后
print("-----end-----")
運行結果:
----start----
0開始執行,進程號為21466
1開始執行,進程號為21468
2開始執行,進程號為21467
0 執行完畢,耗時1.01
3開始執行,進程號為21466
2 執行完畢,耗時1.24
4開始執行,進程號為21467
3 執行完畢,耗時0.56
5開始執行,進程號為21466
1 執行完畢,耗時1.68
6開始執行,進程號為21468
4 執行完畢,耗時0.67
7開始執行,進程號為21467
5 執行完畢,耗時0.83
8開始執行,進程號為21466
6 執行完畢,耗時0.75
9開始執行,進程號為21468
7 執行完畢,耗時1.03
8 執行完畢,耗時1.05
9 執行完畢,耗時1.69
-----end-----
multiprocessing.Pool常用函數解析:
apply_async(func[, args[, kwds]]) :使用非阻塞方式調用func(并行執行,堵塞方式必須等待上一個進程退出才能執行下一個進程),args為傳遞給func的參數列表,kwds為傳遞給func的關鍵字參數列表;
apply(func[, args[, kwds]]):使用阻塞方式調用func
close():關閉Pool,使其不再接受新的任務;
terminate():不管任務是否完成,立即終止;
join():主進程阻塞,等待子進程的退出, 必須在close或terminate之后使用;
apply堵塞式
from multiprocessing import Pool
import os,time,random
def worker(msg):
t_start = time.time()
print("%s開始執行,進程號為%d"%(msg,os.getpid()))
#random.random()隨機生成0~1之間的浮點數
time.sleep(random.random()*2)
t_stop = time.time()
print(msg,"執行完畢,耗時%0.2f"%(t_stop-t_start))
po=Pool(3) #定義一個進程池,最大進程數3
for i in range(0,10):
po.apply(worker,(i,))
print("----start----")
po.close() #關閉進程池,關閉后po不再接收新的請求
po.join() #等待po中所有子進程執行完成,必須放在close語句之后
print("-----end-----")
運行結果:
0開始執行,進程號為21532
0 執行完畢,耗時1.91
1開始執行,進程號為21534
1 執行完畢,耗時1.72
2開始執行,進程號為21533
2 執行完畢,耗時0.50
3開始執行,進程號為21532
3 執行完畢,耗時1.27
4開始執行,進程號為21534
4 執行完畢,耗時1.05
5開始執行,進程號為21533
5 執行完畢,耗時1.60
6開始執行,進程號為21532
6 執行完畢,耗時0.25
7開始執行,進程號為21534
7 執行完畢,耗時0.63
8開始執行,進程號為21533
8 執行完畢,耗時1.21
9開始執行,進程號為21532
9 執行完畢,耗時0.60
----start----
-----end-----
8、進程間通信-Queue
Process之間有時需要通信,操作系統提供了很多機制來實現進程間的通信。
- Queue的使用
可以使用multiprocessing模塊的Queue實現多進程之間的數據傳遞,Queue本身是一個消息列隊程序,首先用一個小實例來演示一下Queue的工作原理:
#coding=utf-8
from multiprocessing import Queue
q=Queue(3) #初始化一個Queue對象,最多可接收三條put消息
q.put("消息1")
q.put("消息2")
print(q.full()) #False
q.put("消息3")
print(q.full()) #True
#因為消息列隊已滿下面的try都會拋出異常,第一個try會等待2秒后再拋出異常,第二個Try會立刻拋出異常
try:
q.put("消息4",True,2)
except:
print("消息列隊已滿,現有消息數量:%s"%q.qsize())
try:
q.put_nowait("消息4")
except:
print("消息列隊已滿,現有消息數量:%s"%q.qsize())
#推薦的方式,先判斷消息列隊是否已滿,再寫入
if not q.full():
q.put_nowait("消息4")
#讀取消息時,先判斷消息列隊是否為空,再讀取
if not q.empty():
for i in range(q.qsize()):
print(q.get_nowait())
運行結果:
False
True
消息列隊已滿,現有消息數量:3
消息列隊已滿,現有消息數量:3
消息1
消息2
消息3
說明
初始化Queue()對象時(例如:q=Queue()),若括號中沒有指定最大可接收的消息數量,或數量為負值,那么就代表可接受的消息數量沒有上限(直到內存的盡頭);
Queue.qsize():返回當前隊列包含的消息數量;
Queue.empty():如果隊列為空,返回True,反之False ;
Queue.full():如果隊列滿了,返回True,反之False;
Queue.get([block[, timeout]]):獲取隊列中的一條消息,然后將其從列隊中移除,block默認值為True;
1)如果block使用默認值,且沒有設置timeout(單位秒),消息列隊如果為空,此時程序將被阻塞(停在讀取狀態),直到從消息列隊讀到消息為止,如果設置了timeout,則會等待timeout秒,若還沒讀取到任何消息,則拋出"Queue.Empty"異常;
2)如果block值為False,消息列隊如果為空,則會立刻拋出"Queue.Empty"異常;
Queue.get_nowait():相當Queue.get(False);
Queue.put(item,[block[, timeout]]):將item消息寫入隊列,block默認值為True;
1)如果block使用默認值,且沒有設置timeout(單位秒),消息列隊如果已經沒有空間可寫入,此時程序將被阻塞(停在寫入狀態),直到從消息列隊騰出空間為止,如果設置了timeout,則會等待timeout秒,若還沒空間,則拋出"Queue.Full"異常;
2)如果block值為False,消息列隊如果沒有空間可寫入,則會立刻拋出"Queue.Full"異常;
Queue.put_nowait(item):相當Queue.put(item, False);
- Queue實例
我們以Queue為例,在父進程中創建兩個子進程,一個往Queue里寫數據,一個從Queue里讀數據:
運行結果:
- 進程池中的Queue
如果要使用Pool創建進程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否則會得到一條如下的錯誤信息:
RuntimeError: Queue objects should only be shared between processes through inheritance.
下面的實例演示了進程池中的進程如何通信:
#coding=utf-8
#修改import中的Queue為Manager
from multiprocessing import Manager,Pool
import os,time,random
def reader(q):
print("reader啟動(%s),父進程為(%s)"%(os.getpid(),os.getppid()))
for i in range(q.qsize()):
print("reader從Queue獲取到消息:%s"%q.get(True))
def writer(q):
print("writer啟動(%s),父進程為(%s)"%(os.getpid(),os.getppid()))
for i in “lawang":
q.put(i)
if __name__=="__main__":
print("(%s) start"%os.getpid())
q=Manager().Queue() #使用Manager中的Queue來初始化
po=Pool()
#使用阻塞模式創建進程,這樣就不需要在reader中使用死循環了,可以讓writer完全執行完成后,再用reader去讀取
po.apply(writer,(q,))
po.apply(reader,(q,))
po.close()
po.join()
print("(%s) End"%os.getpid())
運行結果:
(21156) start
writer啟動(21162),父進程為(21156)
reader啟動(21162),父進程為(21156)
reader從Queue獲取到消息:l
reader從Queue獲取到消息:a
reader從Queue獲取到消息:o
reader從Queue獲取到消息:w
reader從Queue獲取到消息:a
reader從Queue獲取到消息:n
reader從Queue獲取到消息:g
(21156) End
9、多線程-threading
python的thread模塊是比較底層的模塊,python的threading模塊是對thread做了一些包裝的,可以更加方便的被使用
- 使用threading模塊
單線程執行
#coding=utf-8
import time
def saySorry():
print("親愛的,我錯了,我能吃飯了嗎?")
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
saySorry()
運行結果:
多線程執行
#coding=utf-8
import threading
import time
def saySorry():
print("親愛的,我錯了,我能吃飯了嗎?")
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=saySorry)
t.start() #啟動線程,即讓線程開始執行
運行結果:
說明
可以明顯看出使用了多線程并發的操作,花費時間要短很多
創建好的線程,需要調用start()方法來啟動
- 主線程會等待所有的子線程結束后才結束
#coding=utf-8
import threading
from time import sleep,ctime
def sing():
for i in range(3):
print("正在唱歌...%d"%i)
sleep(1)
def dance():
for i in range(3):
print("正在跳舞...%d"%i)
sleep(1)
if __name__ == '__main__':
print('---開始---:%s'%ctime())
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
#sleep(5) # 屏蔽此行代碼,試試看,程序是否會立馬結束?
print('---結束---:%s'%ctime())
- 查看線程數量
#coding=utf-8
import threading
from time import sleep,ctime
def sing():
for i in range(3):
print("正在唱歌...%d"%i)
sleep(1)
def dance():
for i in range(3):
print("正在跳舞...%d"%i)
sleep(1)
if __name__ == '__main__':
print('---開始---:%s'%ctime())
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
while True:
length = len(threading.enumerate())
print('當前運行的線程數為:%d'%length)
if length<=1:
break
sleep(0.5)