01. 進程與程序
編寫完畢的代碼,在沒有運行的時候,稱之為程序
正在運行着的代碼,就稱為進程
進程是系統分配資源的最小單位。
進程資源包括:
中間變量
代碼
計數器
02. 通過os.fork()函數在程序中創建進程
示例:
import os
import time
ret = os.fork() # 創建新的進程 一次調用,兩次返回
if ret == 0:
for i in range(3):
print("放音樂")
time.sleep(0.1)
else:
for i in range(3):
print("跳舞")
time.sleep(0.1)
說明:
1. 程序執行到os.fork()時,操作系統會創建一個新的進程(子進程),然后復制父進程的所有信息到子進程中
2. 然后父進程和子進程都會從fork()函數中得到一個返回值,在子進程中這個值一定是0,而父進程中是子進程的pid號
3. 該函數為操作系統提供只能在Unix/Linux操作系統中使用,而不能在windows中使用。
03. 獲取進程編號
1. Python中的方法
os.getpid() 獲取當前進程的pid
os.getppid() 獲取當前進程的父進程的pid
2. Linux中的方法
ps -aux 獲取當前進程的pid
ps -ef 可以查看到父進程的pid
04. 資源回收
01分配給子進程的資源由父進程負責回收
倘若子進程沒有運行完,父進程運行結束退出,留下的子進程被稱為孤兒進程。父進程死掉后,孤兒進程會被別的進程收養,通常是init進程(pid為1),資源會被繼父進程回收,所以沒什么危害.
倘若子進程先運行完,而父進程並沒有回收資源,那么這個子進程就被稱為僵屍進程。僵屍進程會造成資源浪費。
02. 父進程回收子進程資源的方法
pid, result = os.wait()
返回值pid表示回收的子進程的pid
result表示子進程的退出狀態,通常0表示正常運行完退出
該函數是阻塞方式運行,直到子進程運行結束
05. 多個進程修改全局變量的問題
由於子進程的資源來源於父進程的拷貝,所以父子進程都分配有自己的存儲空間,在進程修改全局變量,並不會影響另一個進程內變量的值。
06. 通過Process類創建進程
01. 介紹
Process類位於multiprocessing模塊中,是Python封裝好的可以跨平台的類。
02. 創建對象
p = Process(target=fn, args=(),kwargs={},name="")
target參數表示創建的進程要執行的函數
args參數表示fn函數所需要的參數
kwargs表示fn函數所需要的關鍵字參數
name參數為當前創建的進程起一個別名,可以通過p.name訪問得到
03. Process對象常見的屬性和方法
p.name 獲取進程的別名
p.pid 獲取進程的pid
p.start() 真正創建出子進程,啟動子進程
p.is_alive() 查看子進程是否存活,返回True或者False
p.terminate() 無論子進程是否執行完,立即終止。存在延遲,這是因為子進程創建后就作為一個單獨的進程,要想在父進程中結束,需要父進程告訴操作系統,操作系統在下次調度子進程時結束掉子進程
p.join() 回收子進程的資源,阻塞,指導子進程執行完
04. 示例
from multiprocessing import Process
import os
import time
def sub_process_fun(num, a):
for i in range(10):
print("子進程:hello")
time.sleep(0.1)
def main():
p = Process(target=sub_process_fun, args=(100,), kwargs={"a": 200}) # 創建一個子進程對象
p.start() # 真正的創建出子進程,子進程可以開始執行代碼
p.join() # 回收子進程資源 阻塞
if __name__ == '__main__':
main()
07. 通過繼承Process類創建進程
創建一個類繼承自Process類,然后把子進程要執行的任務放在run方法中即可。
示例:
class SubProcess(Process):
def __init__(self, num, a):
super(SubProcess, self).__init__() # 執行父類Process默認的初始化方法,通過父類的方法,將子進程對象初始化好
self.num = num
self.a = a
def run(self):
"""子進程要執行的代碼"""
print("子進程:pid=%d" % os.getpid())
print("子進程:num=%d" % self.num)
print("子進程:a=%d" % self.a)
for i in range(10):
print("子進程:hello")
time.sleep(0.1)
def main():
p = SubProcess(100, 200)
p.start() # 真正的創建出子進程,子進程可以開始執行代碼
time.sleep(0.1)
p.terminate() # 終止子進程的執行 存在延遲
p.join() # 回收子進程資源 阻塞
if __name__ == '__main__':
main()
08. 進程池
01. 介紹
當程序中需要大量創建進程時就可以使用進程池來維護這些進程
進程池中會創建指定數量的進程,每次交給進程池任務后,進程池會
自動選擇一個空閑的進程來運行交給的任務。
Python中的進程池通過使用multiprocessing模塊的Pool類來實現
02. 創建進程池對象
pool = Pool(3)
參數3表示創建的進程池中最大進程數
03. 進程池對象的常見方法和屬性
01. pool.apply_async(fn, args, callback)
用空閑出來的子進程去調用目標fn,非阻塞
fn 表示需要進程執行的任務函數
args 表示fn函數的參數,是一個元組
callback 異步任務結束后會自動調用callback參數對應的函數,同時把異步任務fn的返回值傳遞給callback的參數。callback是由主進程調用的。
02. pool.apply(fn, args)
用空閑出來的子進程去調用目標fn,阻塞
03. pool.close()
關閉進程池,關閉后po不再接收新的請求
04. pool.join()
等待po中所有子進程執行完成,必須放在close語句之后,阻塞方式
04. 注意點
應該先定義任務,然后定義進程池對象。這是因為定義進程池對象的時候會
創建子進程,同時把父進程的資源復制一份給子進程,倘若此時任務函數還
沒有定義,子進程在調用函數的時候就會出現找不到的情況。
09. 異步 同步
同步: 從多任務的角度出發,多個任務之間執行的時候要求有先后順序,必須一個先執行完成之后,另一個才能繼續執行, 只有一個主線。即子線程執行的時候主線程會停止等待。
異步: 從多任務的角度出發,多個任務之間執行沒有先后順序,可以同時運行,執行的先后順序不會有什么影響,存在的多條運行主線。即主線程和子線程可以同時執行。
阻塞: 從調用者的角度出發,如果在調用的時候,被卡住,不能再繼續向下運行,需要等待,就說是阻塞
非阻塞: 從調用者的角度出發, 如果在調用的時候,沒有被卡住,能夠繼續向下運行,無需等待,就說是非阻塞
同步和異步是從整個過程中間是否有等待的角度來看
阻塞和非阻塞是從拿到結果之前的狀態的角度來看。
10. Queue類
01. 介紹
multiprocessing類的Queue類,提供隊列的功能,該功能由操作系統提供,由Python進行封裝而成
Queue對象不能當做一個普通的變量來看待,由於它是由操作系統提供,數據並不保存在進程內部,進程內的變量只是該對象的引用,所以可以實現多個進程間的通信。
02. 使用
1. q = Queue(3) #
構造一個Queue對象,3表示可以向隊列中put3條消息,如果不傳參數,則表示可以put任意條消息。
2. q.put(obj, timeout=3)
往隊列中添加數據。如果沒有timeout參數,並且隊列已經滿了,則會出現阻塞直到隊列中有空閑位置。
timeout表示最長等待時間。如果隊列中數據已滿,則最多等待3秒,如果3秒內不能添加數據,則拋出異常
3. obj = q.get(timeout=n)
從隊列中取數據,如果隊列中沒有數據,則等待n秒,n秒內沒有取到數據,則會拋出異常
如果沒有timeout參數,則會阻塞在這里一直等待直到隊列中有數據。
4. q.task_done() 一般配合get使用,讓隊列計數減1
4. q.put_nowait()
向隊列中添加數據,如果隊列已滿,則立即拋出異常
5. obj = q.get_nowait()
從隊列中取數據,如果隊列中沒有數據,則立即拋出異常
6. q.full()
查看隊列是否已滿 返回True或者FALSE
7. q.empty()
查看隊列是否是空的。返回True或者False
8. q.qsize()
當前隊列中數據的個數。mac中不支持。
9. q.join()
多線程中,讓主線程等待隊列中的事情做完然后再退出。
03. 通過Queue對象實現進程間通訊
queue = Queue(3) # 參數表明這個隊列最多只能保存3條數據
def process_write():
for i in range(3):
queue.put(i)
time.sleep(0.5)
def process_read():
while not queue.empty():
ret = queue.get()
time.sleep(0.1)
p1 = Process(target=process_write)
p2 = Process(target=process_read)
p1.start()
p1.join()
p2.start()
p2.join()
04. 進程池中進程之間的通訊,不能使用Queue類,而是使用multiprocessing模塊提供的Manager類
對象的Queue()方法來創建一個隊列對象。其他使用規則一樣。
11. 線程介紹
可以簡單理解為同一進程中有多個計數器
線程內每個線程的執行時間不確定,而每個進程的時間片相等
線程是操作系統調度執行的最小單位
12. 進程線程的區別與優缺點
1. 定義的不同
進程是系統進行資源分配的最小單位.
線程是進程的一個實體,是CPU進行調度的基本單位,
它是比進程更小的能獨立運行的基本單位.
線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),
但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源.
2. 區別
一個程序至少有一個進程,一個進程至少有一個線程.
線程的划分尺度小於進程(資源比進程少),使得多線程程序的並發性高。
進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率
線線程不能夠獨立執行,必須依存在進程中
3. 優缺點
線程和進程在使用上各有優缺點:線程執行開銷小,但不利於資源的管理和保護;而進程正相反。
13. 多線程的實現
通過threading模塊的Thread類來實現多線程
1. 通過實例化一個Thread類的一個對象來實現
2. 通過繼承Thread類來實現
3. 實現的所有方法都可Process類的一樣,可以參考Process類
14. 獲取線程信息
通過threading模塊的enumerate()方法在主線程中獲取所有線程信息
返回一個列表,每一個元素是一個線程對象,保存一個線程的基本信息
threading.current_thread()
獲取當前線程信息
15. 線程的幾種狀態
就緒狀態: 創建一個線程對象,調用start方法后,線程就處於就緒狀態
運行狀態: 操作系統調度該線程時,線程就處於運行狀態。
等待(阻塞): 當線程運行中遇到阻塞,操作系統就會立即切換到別的線程去執行,同時這個線程進入阻塞狀態,等待阻塞結束后,重新進入就緒狀態,等待操作系統下次調度。所以線程的執行時間不均等。
死亡狀態: 線程調度運行結束后進入死亡狀態
16. 注意點
對於通過Thread類或者Process類實現的多線程或者多進程,
如果主線程(進程)沒有執行join操作,則主線程(進程)執行完也會
等待子線程(進程)執行完后一塊退出。
這是由於Python封裝實現的,而底層的os.walk()則不會這樣
17. 多線程修改全局變量
由於多個線程共享同一進程內的資源,
所以多線程修改全局變量會出現資源競爭的問題。
18. 使用互斥鎖解決多線程資源競爭問題
1. 通過使用threading模塊的Lock類來實現
2. Lock類對象的使用方法
1. mutex = Lock()
創建一個互斥鎖對象
2. mutex.acquire(blocking=True, timeout=None)
給互斥鎖上鎖
blocking表示是以阻塞或者非阻塞的方式運行上鎖
True 默認 以阻塞的方式運行
首先判斷當前互斥鎖的狀態,
如果是上鎖狀態,則阻塞等到鎖為未上鎖狀態,由於發生阻塞,操作系統會立即切換到別的線程執行
如果是未上鎖狀態,則將互斥鎖設置為上鎖狀態
總是返回True
如果指定了timeout參數,則表示最長等待時間,超過這個時間仍未上鎖成功,就不再等待,直接返回一個false值
FALSE 表示以非阻塞方式運行該方法
運行該方法會直接返回結果,如果上鎖成功,則返回True,上鎖失敗,返回FALSE
注:
非阻塞方式可以通過對返回值的判斷而知道是否上鎖成功,這樣就可以在上鎖不成功的情況
下做一些其他的事情而不用總是等待在這里。
3. mutex.release()
釋放鎖
將互斥鎖的狀態設置為打開。
19. 多線程修改局部變量
通過繼承Thread類得到一個子類,
然后通過該子類構造多個對象,讓這幾個對象同時開啟子線程
或者定義一個函數,多個線程同時運行該函數,
通過在函數內修改函數的局部變量或者子類的run方法內修改對象的屬性,
多個線程直接不會互相影響修改的結果。
20. 線程死鎖
1. 起因
線程中存在多個互斥鎖,多個線程都需要對方的資源即每個線程都需要對方的鎖先打開,就會出現死鎖現象
2. 避免死鎖的方法
1. 添加超時時間
給acquire方法添加timeout參數,過了超時時間還拿不到鎖,就把自己的互斥鎖釋放掉
2. 銀行家算法
該算法是從資源分配者的角度出發,
先把資源分配給需求最少的客戶,等這個客戶用完把所有資源歸還后,
再分配給需求大一點的客戶
21. 同步應用(多個線程按照一定的順序執行)
通過互斥鎖來控制線程的運行順序
即一個線程運行完並不釋放自己的互斥鎖,
而是釋放下一個線程要用到的互斥鎖,
下一個線程就會得到互斥鎖,
執行完后也不釋放自己的互斥鎖,同樣釋放下一個想要執行的線程的互斥鎖,
最后一個線程釋放第一個線程的鎖。
程序開始時,把第一個要執行的線程的互斥鎖打開,其他的全部上鎖。
22. 生產者消費者模式
所謂生產者消費者模式,放在線程中就是一個線程用來產生數據,
而另一個線程用來處理數據。同時為了減少這兩個線程之間的耦合度,
可以引入一個倉庫,生產者和消費者都操作這個倉庫。
倉庫可以使用queue模塊的Queue類來模擬,使用方法和multiprocessing模塊的Queue類一樣。
23. GIL(全局解釋器鎖)
全局解釋器鎖導致Python中的多線程並不能同時運行
在Python中創建一個子線程相當於在Python解釋器的進程
中創建一個線程,而所有的代碼要運行都需要用到解釋器的一些全局資源,
而為了讓每個線程都能安全的運行,就出現了GIL,每個線程或者每段代碼在運行的時候都需要拿到GIL才能運行,
這就讓Python的多線程並不是真正的多個線程在同時執行,而是交替進行的。
當然這里所說的子線程或者代碼是指計算型的代碼,涉及到io型的代碼並不需要拿到GIL就可以執行,因為io操作一般都是調用底層C語言的代碼,
調用之前就會自動釋放掉GIL鎖,這個時候其他的線程就可以拿到GIL來運行
所以在涉及IO操作的部分,可以使用多線程來提高效率。
GIL問題只有在C語言實現的cpython解釋器中有,別的版本的Python解釋器如pypy,jpython中並沒有這個問題。
24. 多線程多進程的使用地方
一般CPU密集型即需要大量計算的時候考慮使用多進程以充分利用CPU
IO密集型即文件讀寫,網絡訪問,數據庫訪問時考慮使用多線程。