「Python網絡編程」再識多任務的真面目/多進程(三)

博主前言:

上篇博客我講述了多任務的一種實現方式—多線程。這篇博客我繼續講述使用進程的方式來實現多任務。由於進程的知識實屬有點抽象,特別是結合線程來講進程確實有點難度,所以在本篇博客開寫之前,我借閱了多個論壇大佬寫的有關於多進程的博客,所以這篇博客如有雷同,算我抄你的。但是我保證,百分之一大半都是自己理解所得。

1. 多進程

在「Python網絡編程」系列第一篇博客講端口號時,我們瞭解了有關進程的概念。進程是一個具有一定獨立功能的程序關於某個數據集合的一次運行活動,是操作系統分配資源的基本單元,是操作系統結構的基礎。
如果你覺得上段黑體字有點難以理解,那麼你可以就把進程理解爲運行着的程序也可。程序與進程的區別就是:程序是靜態的,進程是動態的。
我在網上看到一個公式覺得非常精闢:進程 = 程序 + 資源

1.1 進程的狀態

在這裏插入圖片描述
1)就緒狀態(Ready):
進程已獲得除處理器外的所需資源,等待分配處理器資源;只要分配了處理器進程就可執行。就緒進程可以按多個優先級來劃分隊列。例如,當一個進程由於時間片用完而進入就緒狀態時,排入低優先級隊列;當進程由I/O操作完成而進入就緒狀態時,排入高優先級隊列。
結合上面的公式可以理解爲,就緒狀態就是程序運行的資源已經準備好,等待CPU執行。
2)運行狀態(Running):
進程佔用處理器資源;處於此狀態的進程的數目小於等於處理器的數目。在沒有其他進程可以執行時(如所有進程都在阻塞狀態),通常會自動執行系統的空閒進程。
結合上面的公式可以理解爲,運行狀態就是CPU正在執行該進程。
3)阻塞狀態(Blocked):
由於進程等待某種條件(如I/O操作或進程同步),在條件滿足之前無法繼續執行。該事件發生前即使把處理器資源分配給該進程,也無法運行。
結合上面的公式可以理解爲,阻塞狀態就是進程等待運行的資源滿足,(此時進程的資源還沒有準備好)。

1.2 進程與線程的區別

⑴. 進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。
⑵. 一個程序至少有一個進程,一個進程至少有一個線程。
⑶. 進程之間相互獨立,不共享全局變量;線程之間可以共享全局變量。
⑷. 線程的劃分尺度小於進程,而多個線程共享內存,從而提高了程序的運行效率。
⑸. 在調度時,線程的調度快過進程的調度。
通過下圖可以充分的理解上述區別,請讀者細細理解。
在這裏插入圖片描述

2. multiprocessing模塊

通過Python的multiprocessing模塊,我們可以創建多進程來實現多任務。
標識每個進程的值稱爲進程標識符,用PID表示。
這裏的PID不是我們講過的端口號,讀者一定要注意區分。
只要運行一程序,系統會自動分配一個PID值給它。
PID值是暫時唯一的,進程中止後,這個號碼就會被回收,並可能被分配給另一個新進程。
在這裏插入圖片描述接下來,我們使用multiprocessing模塊來創建多進程。

import multiprocessing
import os
import time

def test1():
	"""測試函數1"""
    print("子進程1的PID號是:%d" %os.getpid())
    while True:
        print("This is test1")
        time.sleep(2)

def test2():
	"""測試函數2"""
    print("子進程2的PID號是:%d" %os.getpid())
    while True:
        print("This is test2")
        time.sleep(2)

def main():
	# 使用multiprocessing模塊的Process類來創建子進程對象
    t1 = multiprocessing.Process(target=test1)
    t2 = multiprocessing.Process(target=test2)
    # 調用start函數開啓子進程
    t1.start()
    t2.start()
    print("子進程1的PID號是:"+str(t1.pid))
    print("子進程2的PID號是:"+str(t2.pid))
    print("總進程的PID號是:%d" %os.getpid())

if __name__ == '__main__':
    main()

上述代碼通過multiprocessing模塊的Process類來創建兩個子進程,其中target指向的參數就是子進程需要執行的代碼塊。其用法與線程threading模塊用法一樣,所以不再過多講述。
在這裏我通過兩種方式來獲得進程號PID值。

  1. 引入os模塊,調用os模塊的getpid的函數獲取PID值。
  2. 通過調用子進程對象的pid屬性來獲取PID值。

運行上述代碼,我們可得結果如下圖所示。
在這裏插入圖片描述
在這時我們打開任務管理器,點到詳細信息,可以看到這時正在運行的python程序有三個。
其中有一個就是主進程,另外兩個就是我們創建的子進程。
在這裏插入圖片描述

3. 進程間通信

由於多進程之間是相互獨立的,不能共享全局變量,所以進程之間是怎樣通信的呢?
其實我們已經學過了其中一種通信的方式——套接字。
socket編程就是我們將要通信的數據發送到網絡,通過網絡(各種交換機、路由器)實現不同進程之間的通信的過程。
實現進程間通信有許多種方式,如圖所示。
在這裏插入圖片描述

3.1 消息隊列Queue

通過multiprocessing模塊的Queue對象可以實現進程間通信。
Queue,翻譯過來就是隊列的意思。
在《數據結構》中我們學習過有關樹、隊列、棧等相關知識。什麼雙端隊列、循環隊列、線索二叉樹、平衡二叉樹等等。在這裏我就不詳細的展開,若有需求請自行CSDN。
閱讀下面的內容只需要知道隊列的特點是先進先出,棧的特點是後進先出即可。

import multiprocessing

def download_from_server(queue):
    """模擬從網上下載數據"""
    datas = [11,22,33,44,55]
    for data in datas:
    	# 通過put函數,將數據加入隊列
        queue.put(data)	

def analysis_data(queue):
    """模擬數據處理"""
    while True:
    	# 通過get_nowait函數,取出隊列中最前面的數據
        data = queue.get_nowait()
        print(data)
        # 通過empty函數檢測隊列的元素個數是否爲0,返回值爲布爾型。
        if queue.empty():
            print("當前隊列爲空")
            break


def main():
 	# 初始化一個Queue對象,最多可接受5條信息
    queue = multiprocessing.Queue(5) 
    t1 = multiprocessing.Process(target=download_from_server,args=(queue,))
    t2 = multiprocessing.Process(target=analysis_data,args=(queue,))
    t1.start()
    t2.start()

if __name__ == '__main__':
    main()

首先,我們使用multiprocessing模塊的Queue創建一個Queue對象。
然後,在子進程1中將要通信的數據通過put函數加入隊列。
最後,在子進程2中通過get_nowait函數取出數據進行處理。
將數據放進隊列可用:Queue.put()、Queue.put_nowait()
將數據從隊列中取出:Queue.get()、Queue.get_nowait()
判斷隊列是否爲滿:Queue.full()
判斷隊列是否爲空:Queue.empty()
「put函數和put_nowait函數的差別就是,當隊列元素滿的時候,再向隊列加入元素是否會報錯,同理get函數和get_nowait函數也是如此」

3.2 進程池

如果有大量任務需要多進程完成,則可能需要頻繁的創建刪除進程,這樣會帶來較多的資源消耗。
這種情況,我們創建適當的進程放入進程池,用來處理待處理事件,處理完畢後進程不銷燬,仍然在進程池中等待處理其他事件,直到事件全部處理完畢,進程退出。 通過進程的複用降低資源的消耗。

import multiprocessing
import os,time,random

def test(msg):
	"""測試函數"""
    t_start = time.time()
    print("%s開始執行,進程號爲%d"%(msg,os.getpid()))
    time.sleep(random.random()*2)
    t_stop = time.time()
    run_time = t_stop-t_start
    print(msg,"執行完畢,耗時%0.2f" %run_time)

def main():
	# 創建進程池對象
    pool = multiprocessing.Pool(3)
    for i in range(10):
    	# 將事件異步加入進程池
        pool.apply_async(test,(i,))
    # 關閉進程池,此時不能再將事件加入進程池
    pool.close()
    # 此時主進程會形成堵塞,會等待進程池中的任務全部執行完主進程纔會解堵塞
    pool.join()

if __name__ == '__main__':
    main()

通過multiprocessing模塊Pool類創建進程池對象,創建時在池內放入合適數量的進程(默認爲CPU的個數),
然後通過apply_async函數將事件加入進程池的等待隊列,使用進程池內的進程不斷的執行等待事件,
直到所有事件執行完畢,所有事件處理完畢後,關閉進程池,回收進程池。

將事件加入進程池有兩種函方式:

  • apply 同步提交,直接返回結果。
  • apply_async 異步提交,返回對象,通過對象獲取返回值,異步提交時要用Pool.close() 和Pool.join()。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章