網站首頁 編程語言 正文
NICE!大家好,在上一章節,我們學習了 multiprocessing 模塊 的關于進程的創建與進場常用的方法的相關知識。 通過在一個主進程下創建多個子進程可以幫助我們加速程序的運行,并且提高工作效率。不過上一章節文末我們也說過進程的問題,由于每一個進程都會消耗 CPU 與 內存 資源,這樣就不能無限的創建進程的問題,因為會造成內存不足或者死機的情況。
為了解決這個問題我們可以使用多線程來替代,或者使用我們今天要學習的內容 —> 進程池。不僅如此,我們在上一章節也說了另一個問題,多進程在同時修改一個文件的時候可能會存在問題,解決的方法就是給這一個文件進行 上鎖 。今天我們就來學習一下 進程池與進程鎖 ,看看它們都能幫助我們怎樣解決問題。
進程池
什么是進程池
上一章節關于進程的問題我們提到過,進程創建太多的情況下就會對資源消耗過大。為了避免出現這種情況,我們就需要固定進程的數量,這時候就需要進程池的幫助。
我們可以認為進程池就是一個池子,在這個池子里提前創建好一定數量的進程。見下圖:
比如這個紅色矩形陣列就代表一個進程池子,在這個池子中有6個進程。這6個進程會伴隨進程池一起被創建,不僅如此,我們在學習面向對象的生命周期的時候曾經說過,每個實例化對象在使用完成之后都會被內存管家回收。
我們的進程也會伴隨著創建與關閉的過程而被內存管家回收,每一個都是如此,創建于關閉進程的過程也會消耗一定的性能。而進程池中的進程當被創建之后就不會被關閉,可以一直被重復使用,從而避免了創建于關閉的資源消耗,也避免了創建于關閉的反復操作提高了效率。
當然,當我們執行完程序進程池關閉的時候,進程也隨之關閉。
當我們有任務需要被執行的時候,會判斷當前的進程池當中有沒有空閑的進程(所謂空閑的進程其實就是進程池中沒有執行任務的進程)。有進程處于空閑狀態的情況下,任務會找到進程執行該任務。如果當前進程池中的進程都處于非空閑狀態,則任務就會進入等待狀態,直到進程池中有進程處于空閑狀態才會進出進程池從而執行該任務。
這就是進程池的作用。
進程池的創建模塊 - multiprocessing
創建進程池函數 - Pool
函數名 | 介紹 | 參數 | 返回值 |
---|---|---|---|
Pool | 進程池的創建 | Processcount | 進程池對象 |
Pool功能介紹:通過調用 "multiprocessing" 模塊的 "Pool" 函數來幫助我們創建 "進程池對象" ,它有一個參數 "Processcount" (一個整數),代表我們這個進程池中創建幾個進程。
進程池的常用方法
當創建了進程池對象之后,我們要對它進程操作,讓我們來看一下都有哪些常用方法(函數)。
函數名 | 介紹 | 參數 | 返回值 |
---|---|---|---|
apply_async | 任務加入進程池(異步) | func,args | 無 |
close | 關閉進程池 | 無 | 無 |
join | 等待進程池任務結束 | 無 | 無 |
- apply_async 函數:它的功能是將任務加入到進程池中,并且是通過異步實現的。異步 這個知識我們還沒有學習,先不用關心它到底是什么意思。它有兩個參數:func 與 agrs , func 是加入進程池中工作的函數;args 是一個元組,代表著簽一個函數的參數,這和我們創建并使用一個進程是完全一致的。
- close 函數:當我們使用完進程池之后,通過調用 close 函數可以關閉進程池。它沒有任何的參數,也沒有任何的返回值。
- join 函數:它和我們上一章節學習的 創建進程的 join 函數中方法是一致的。只有進程池中的任務全部執行完畢之后,才會執行后續的任務。不過一般它會伴隨著進程池的關閉(close 函數)才會使用。
apply_async 函數演示案例
接下里我們在 Pycharm 中創建一個腳本,練習一下關于進程池的使用方法。
- 定義一個函數,打印輸出該函數 每次被執行的次數 與 該次數的進程號
- 定義進程池的數量,每一次的執行進程數量最多為該進程池設定的進程數
示例代碼如下:
# coding:utf-8 import os import time import multiprocessing def work(count): # 定義一個 work 函數,打印輸出 每次執行的次數 與 該次數的進程號 print('\'work\' 函數 第 {} 次執行,進程號為 {}'.format(count, os.getpid())) time.sleep(3) # print('********') if __name__ == '__main__': pool = multiprocessing.Pool(3) # 定義進程池的進程數量,同一時間每次執行最多3個進程 for i in range(21): pool.apply_async(func=work, args=(i,)) # 傳入的參數是元組,因為我們只有一個 i 參數,所以我們要寫成 args=(i,) time.sleep(15) # 這里的休眠時間是必須要加上的,否則我們的進程池還未運行,主進程就已經運行結束,對應的進程池也會關閉。
運行結果如下:
從上圖中我們可以看到每一次都是一次性運行三個進程,每一個進程的進程號是不一樣的,但仔細看會發現存在相同的進程號,這說明進程池的進程號在被重復利用。這證明我們上文介紹的內容,進程池中的進程不會被關閉,可以反復使用。
而且我們還可以看到每隔3秒都會執行3個進程,原因是我們的進程池中只有3個進程;雖然我們的 for 循環 中有 21 個任務,work 函數會被執行21次,但是由于我們的進程池中只有3個進程。所以當執行了3個任務之后(休眠3秒),后面的任務等待進程池中的進程處于空閑狀態之后才會繼續執行。
同樣的,進程號在順序上回出現一定的區別,原因是因為我們使用的是一種 異步 的方法(異步即非同步)。這就導致 work 函數 一起執行的三個任務會被打亂順序,這也是為什么我們的進程號出現順序不一致的原因。(更多的異步知識我們會在異步的章節進行詳細介紹)
進程池的原理: 上述腳本的案例證實了我們進程池關于進程的限制,只有當我們進程池中的進程處于空閑狀態的時候才會將進程池外等待的任務扔到進程池中工作。
close 函數與 join 函數 演示
在上文的腳本中, 我們使用 time.sleep(15) 幫助我們將主進程阻塞15秒鐘再次退出,所以給了我們進程池足夠的時間完成我們的 work() 函數的循環任務。
如果沒有 time.sleep(15) 這句話又怎么辦呢,其實這里就可以使用進程的 join 函數了。不過上文我們也提到過,進程的 join() 函數一般都會伴隨進程池的關閉(close 函數)來使用。接下來,我們就將上文腳本中的 time.sleep(15) 替換成 join() 函數試一下。
示例代碼如下:
# coding:utf-8 import os import time import multiprocessing def work(count): # 定義一個 work 函數,打印輸出 每次執行的次數 與 該次數的進程號 print('\'work\' 函數 第 {} 次執行,進程號為 {}'.format(count, os.getpid())) time.sleep(3) # print('********') if __name__ == '__main__': pool = multiprocessing.Pool(3) # 定義進程池的進程數量,同一時間每次執行最多3個進程 for i in range(21): pool.apply_async(func=work, args=(i,)) # 傳入的參數是元組,因為我們只有一個 i 參數,所以我們要寫成 args=(i,) # time.sleep(15) pool.close() pool.join()
運行結果如下:
從上面的動圖我們可以看出,work() 函數的任務與進程池中的進程與使用 time.sleep(15)的運行結果一致。
PS:如果我們的主進程會一直執行,不會退出。那么我們并不需要添加 close() 與 join() 函數 ,可以讓進程池一直啟動著,直到有任務進來就會執行。
在后面學習 WEB 開發之后,不退出主進程進行工作是家常便飯。還有一些需要長期執行的任務也不會關閉,但要是只有一次性執行的腳本,就需要添加 close() 與 join() 函數 來保證進程池的任務全部完成之后主進程再退出。當然,如果主進程關閉了,就不會再接受新的任務了,也就代表了進程池的終結。
接下來再看一個例子,在 work 函數 中加入一個 return。
這里大家可能會有一個疑問,在上一章節針對進程的知識點明明說的是 進程無法獲取返回值,那么這里的 work() 函數增加的 return 又有什么意義呢?
其實不然,在我們的使用進程池的 apply_async 方法時,是通過異步的方式實現的,而異步是可以獲取返回值的。針對上述腳本,我們在 for循環中針對每一個異步 apply_async 添加一個變量名,從而獲取返回值。
示例代碼如下:
# coding:utf-8 import os import time import multiprocessing def work(count): # 定義一個 work 函數,打印輸出 每次執行的次數 與 該次數的進程號 print('\'work\' 函數 第 {} 次執行,進程號為 {}'.format(count, os.getpid())) time.sleep(3) return '\'work\' 函數 result 返回值為:{}, 進程ID為:{}'.format(count, os.getpid()) if __name__ == '__main__': pool = multiprocessing.Pool(3) # 定義進程池的進程數量,同一時間每次執行最多3個進程 results = [] for i in range(21): result = pool.apply_async(func=work, args=(i,)) # 傳入的參數是元組,因為我們只有一個 i 參數,所以我們要寫成 args=(i,) results.append(result) for result in results: print(result.get()) # 可以通過這個方式返回 apply_async 的返回值, # 通過這種方式也不再需要 使用 close()、join() 函數就可以正常執行。 # time.sleep(15) # 這里的休眠時間是必須要加上的,否則我們的進程池還未運行,主進程就已經運行結束,對應的進程池也會關閉。 # pool.close() # pool.join()
運行結果如下:
從運行結果可以看出,首先 work() 函數被線程池的線程執行了一遍,當第一組任務執行完畢緊接著執行第二次線程池任務的時候,打印輸出了 apply_async 的返回值,證明返回值被成功的返回了。然后繼續下一組的任務…
這些都是主要依賴于 異步 ,關于 異步 的更多知識會在 異步 的章節進行詳細的介紹。
進程鎖
進程鎖的概念
鎖:大家都知道,我們可以給一個大門上鎖。
結合這個場景來舉一個例子:比如現在有多個進程同時沖向一個 "大門" ,當前門內是沒有 "人"的(其實就是進程),鎖也沒有鎖上。當有一個進程進去之后并且把 “門” 鎖上了,這時候門外的那些進程是進不來的。在門內的 “人” ,可以在 “門” 內做任何事情且不會被干擾。當它出來之后,會解開門鎖。這時候又有一個 “人” 進去了門內,并且重復這樣的操作,這就是 進程鎖。它可以讓鎖后面的工作只能被一個任務來處理,只有它解鎖之后下一個任務才會進入,這就是 “鎖” 的概念。
而 進程鎖 就是僅針對于 進程 有效的鎖,當進程的任務開始之后,就會被上一把 “鎖”;與之對應的是 線程鎖 ,它們的原理幾乎是一樣的。
進程鎖的加鎖與解鎖
進程鎖的使用方法:
通過 multiprocessing 導入 Manager 類
from multiprocessing import Manager
然后實例化 Manager
manager = Manager()
再然后通過實例化后的 manager 調用 它的 Lock() 函數
lock = manager.Lock()
接下來,就需要操作這個 lock 對象的函數
函數名 | 介紹 | 參數 | 返回值 |
---|---|---|---|
acquire | 上鎖 | 無 | 無 |
release | 解鎖(開鎖) | 無 | 無 |
代碼示例如下:
# coding:utf-8 import os import time import multiprocessing def work(count, lock): # 定義一個 work 函數,打印輸出 每次執行的次數 與 該次數的進程號,增加線程鎖。 lock.acquire() # 上鎖 print('\'work\' 函數 第 {} 次執行,進程號為 {}'.format(count, os.getpid())) time.sleep(3) lock.release() # 解鎖 return '\'work\' 函數 result 返回值為:{}, 進程ID為:{}'.format(count, os.getpid()) if __name__ == '__main__': pool = multiprocessing.Pool(3) # 定義進程池的進程數量,同一時間每次執行最多3個進程 manager = multiprocessing.Manager() lock = manager.Lock() results = [] for i in range(21): result = pool.apply_async(func=work, args=(i, lock)) # 傳入的參數是元組,因為我們只有一個 i 參數,所以我們要寫成 args=(i,) # results.append(result) # time.sleep(15) # 這里的休眠時間是必須要加上的,否則我們的進程池還未運行,主進程就已經運行結束,對應的進程池也會關閉。 pool.close() pool.join()
執行結果如下:
從上圖中,可以看到每一次只有一個任務會被執行。由于每一個進程會被阻塞 3秒鐘,所以我們的進程執行的非常慢。這是因為每一個進程進入到 work() 函數中,都會執行 上鎖、阻塞3秒、解鎖 的過程,這樣就完成了一個進程的工作。下一個進程任務開始,重復這個過程… 這就是 進程鎖的概念。
其實進程鎖還有很多種方法,在 multiprocessing 中有一個直接使用的鎖,就是 ``from multiprocessing import Lock。這個Lock的鎖使用和我們剛剛介紹的Manager` 的鎖的使用有所區別。(這里不做詳細介紹,感興趣的話可以自行拓展一下。)
鎖 的使用可以讓我們對某個任務 在同一時間只能對一個進程進行開發,但是 鎖也不可以亂用 。因為如果某些原因造成 鎖沒有正常解開 ,就會造成死鎖的現象,這樣就無法再進行操作了。
因為 鎖如果解不開 ,后面的任務也就沒有辦法繼續執行任務,所以使用鎖一定要謹慎。
OKK,今天我們學習了進程池與鎖的使用方法,通過學習這兩個知識點可以幫助我們解決進程的一些弊端,但它們自身的使用也要注意一些事項。在不同的場景使用的效果也不盡相同,而 鎖的使用 則更需要注意。
原文鏈接:https://blog.csdn.net/weixin_42250835/article/details/124070067
相關推薦
- 2022-04-10 C#實現簡單的計算器小功能_C#教程
- 2022-04-02 教你在windows下搭建MQTT服務器的方法_win服務器
- 2022-12-14 C++利用類實現矩陣的數乘,乘法以及點乘_C 語言
- 2023-07-24 E6新語法for of 和ES3的for in 有什么區別?useState為什么是用數組結構而不用
- 2022-07-03 python中dict獲取關鍵字與值的實現_python
- 2022-03-29 C語言中的盜賊(小偷)問題詳解_C 語言
- 2022-11-14 Android開發RecyclerView單獨刷新使用技巧_Android
- 2022-10-22 如何在Go中使用Casbin進行訪問控制_Golang
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支