41、併發程式設計之多程式實操篇

阿木古冷發表於2020-11-24

一 multiprocessing模組介紹

python中的多執行緒無法利用多核優勢,如果想要充分地使用多核CPU的資源(os.cpu_count()檢視),在Python中大部分情況需要使用多程式。Python提供了multiprocessing。 multiprocessing模組用來開啟子程式,並在子程式中執行我們定製的任務(比如函式),該模組與多執行緒模組threading的程式設計介面類似。

multiprocessing模組的功能眾多:支援子程式、通訊和共享資料、執行不同形式的同步,提供了Process、Queue、Pipe、Lock等元件。

需要再次強調的一點是:與執行緒不同,程式沒有任何共享狀態,程式修改的資料,改動僅限於該程式內。

二 Process類的介紹

建立程式的類

Process([group [, target [, name [, args [, kwargs]]]]]),由該類例項化得到的物件,表示一個子程式中的任務(尚未啟動)

強調:
需要使用關鍵字的方式來指定引數
args指定的為傳給target函式的位置引數,是一個元組形式,必須有逗號

引數介紹:

# group引數未使用,值始終為None
 
# target表示呼叫物件,即子程式要執行的任務

# args表示呼叫物件的位置引數元組,args=(1,2,'amgulen',)

# kwargs表示呼叫物件的字典,kwargs={'name':'amgulen','age':18}

# name為子程式的名稱

方法介紹:

# p.start():啟動程式,並呼叫該子程式中的p.run() 

# p.run():程式啟動時執行的方法,正是它去呼叫target指定的函式,我們自定義類的類中一定要實現該方法   

# p.terminate():強制終止程式p,不會進行任何清理操作,如果p建立了子程式,該子程式就成了殭屍程式,使用該方法需要特別小心這種情況。如果p還儲存了一個鎖那麼也將不會被釋放,進而導致死鎖

# p.is_alive():如果p仍然執行,返回True

# p.join([timeout]):主執行緒等待p終止(強調:是主執行緒處於等的狀態,而p是處於執行的狀態)。timeout是可選的超時時間,需要強調的是,p.join只能join住start開啟的程式,而不能join住run開啟的程式

屬性介紹:

# p.daemon:預設值為False,如果設為True,代表p為後臺執行的守護程式,當p的父程式終止時,p也隨之終止,並且設定為True後,p不能建立自己的新程式,必須在p.start()之前設定
 
# p.name:程式的名稱

# p.pid:程式的pid

# p.exitcode:程式在執行時為None、如果為–N,表示被訊號N結束(瞭解即可)

# p.authkey:程式的身份驗證鍵,預設是由os.urandom()隨機生成的32字元的字串。這個鍵的用途是為涉及網路連線的底層程式間通訊提供安全性,這類連線只有在具有相同的身份驗證鍵時才能成功(瞭解即可)

三 Process類的使用

注意:在windows中Process()必須放到 if __ name __ == ' __ main __ ':下

詳細解釋

Since Windows has no fork, the multiprocessing module starts a new Python process and imports the calling module. 
If Process() gets called upon import, then this sets off an infinite succession of new processes (or until your machine runs out of resources). 
This is the reason for hiding calls to Process() inside

if __name__ == "__main__"
since statements inside this if-statement will not get called upon import.
由於Windows沒有fork,多處理模組啟動一個新的Python程式並匯入呼叫模組。 
如果在匯入時呼叫Process(),那麼這將啟動無限繼承的新程式(或直到機器耗盡資源)。 
這是隱藏對Process()內部呼叫的原,使用if __name__ == “__main __”,這個if語句中的語句將不會在匯入時被呼叫。

建立並開啟子程式的兩種方式

方法一

# 開程式的方法一:
import time
import random
from multiprocessing import Process
def play(name):
    print('%s playing' %name)
    time.sleep(random.randrange(1,5))
    print('%s play end' %name)



p1=Process(target=play,args=('amgulen',)) #必須加,號
p2=Process(target=play,args=('zhu',))
p3=Process(target=play,args=('ya',))
p4=Process(target=play,args=('gou',))

p1.start()
p2.start()
p3.start()
p4.start()
print('主執行緒')

方法二

#開程式的方法二:
import time
import random
from multiprocessing import Process


class Play(Process):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        print('%s playing' %self.name)

        time.sleep(random.randrange(1,5))
        print('%s play end' %self.name)

p1=Play('amgulen')
p2=Play('zhu')
p3=Play('ya')
p4=Play('gou')

p1.start() # start會自動呼叫run
p2.start()
p3.start()
p4.start()
print('主執行緒')

程式直接的記憶體空間是隔離的

from multiprocessing import Process
n=100 # 在windows系統中應該把全域性變數定義在if __name__ == '__main__'之上就可以了
def work():
    global n
    n=0
    print('子程式內: ',n)


if __name__ == '__main__':
    p=Process(target=work)
    p.start()
    print('主程式內: ',n)

練習1:把socket通訊變成併發的形式

server端

from socket import *
from multiprocessing import Process

server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
server.bind(('127.0.0.1',8080))
server.listen(5)

def talk(conn,client_addr):
    while True:
        try:
            msg=conn.recv(1024)
            if not msg:break
            conn.send(msg.upper())
        except Exception:
            break

if __name__ == '__main__': # windows下start程式一定要寫到這下面
    while True:
        conn,client_addr=server.accept()
        p=Process(target=talk,args=(conn,client_addr))
        p.start()

多個client端

from socket import *

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))


while True:
    msg=input('>>: ').strip()
    if not msg:continue

    client.send(msg.encode('utf-8'))
    msg=client.recv(1024)
    print(msg.decode('utf-8'))

這麼實現有沒有問題???

每來一個客戶端,都在服務端開啟一個程式,如果併發來一個萬個客戶端,要開啟一萬個程式嗎,你自己嘗試著在你自己的機器上開啟一萬個,10萬個程式試一試。
解決方法:程式池

Process物件的join方法

join:主程式等,等待子程式結束

from multiprocessing import Process
import time
import random

class Play(Process):
    def __init__(self,name):
        self.name=name
        super().__init__()
    def run(self):
        print('%s is playing' %self.name)
        time.sleep(random.randrange(1,3))
        print('%s is play end' %self.name)


p=Play('amgulen')
p.start()
p.join(0.0001) # 等待p停止,等0.0001秒就不再等了
print('開始')

有了join,程式不就是序列了嗎?

from multiprocessing import Process
import time
import random
def play(name):
    print('%s is playing' %name)
    time.sleep(random.randint(1,3))
    print('%s is play end' %name)

p1=Process(target=play,args=('amgulen',))
p2=Process(target=play,args=('zhu',))
p3=Process(target=play,args=('ya',))
p4=Process(target=play,args=('gou',))

p1.start()
p2.start()
p3.start()
p4.start()

#疑問:既然join是等待程式結束,那麼我像下面這樣寫,程式不就又變成序列的了嗎?
#當然不是了,必須明確:p.join()是讓誰等?
#很明顯p.join()是讓主執行緒等待p的結束,卡住的是主執行緒而絕非程式p,

#詳細解析如下:
#程式只要start就會在開始執行了,所以p1-p4.start()時,系統中已經有四個併發的程式了
#而我們p1.join()是在等p1結束,沒錯p1只要不結束主執行緒就會一直卡在原地,這也是問題的關鍵
#join是讓主執行緒等,而p1-p4仍然是併發執行的,p1.join的時候,其餘p2,p3,p4仍然在執行,等#p1.join結束,可能p2,p3,p4早已經結束了,這樣p2.join,p3.join.p4.join直接通過檢測,無需等待
# 所以4個join花費的總時間仍然是耗費時間最長的那個程式執行的時間
p1.join()
p2.join()
p3.join()
p4.join()

print('主執行緒')


#上述啟動程式與join程式可以簡寫為
# p_l=[p1,p2,p3,p4]
# 
# for p in p_l:
#     p.start()
# 
# for p in p_l:
#     p.join()

Process物件的其他方法或屬性(瞭解)

terminate與is_alive

#程式物件的其他方法一:terminate,is_alive
from multiprocessing import Process
import time
import random

class Play(Process):
    def __init__(self,name):
        self.name=name
        super().__init__()

    def run(self):
        print('%s is playing' %self.name)
        time.sleep(random.randrange(1,5))
        print('%s is play end' %self.name)


p1=Play('amgulen')
p1.start()

p1.terminate()  # 關閉程式,不會立即關閉,所以is_alive立刻檢視的結果可能還是存活
print(p1.is_alive())  # 結果為True

print('開始')
print(p1.is_alive())  # 結果為False

name與pid

from multiprocessing import Process
import time
import random
class Play(Process):
    def __init__(self,name):
        # self.name=name
        # super().__init__() #Process的__init__方法會執行self.name=Piao-1,
        #                    #所以加到這裡,會覆蓋我們的self.name=name

        #為我們開啟的程式設定名字的做法
        super().__init__()
        self.name=name

    def run(self):
        print('%s is playing' %self.name)
        time.sleep(random.randrange(1,3))
        print('%s is play end' %self.name)

p=Play('amgulen')
p.start()
print('開始')
print(p.pid)  # 檢視pid

殭屍程式與孤兒程式(瞭解)

一:殭屍程式(有害)
  殭屍程式:一個程式使用fork建立子程式,如果子程式退出,而父程式並沒有呼叫wait或waitpid獲取子程式的狀態資訊,那麼子程式的程式描述符仍然儲存在系統中。這種程式稱之為僵死程式。詳解如下

我們知道在unix/linux中,正常情況下子程式是通過父程式建立的,子程式在建立新的程式。子程式的結束和父程式的執行是一個非同步過程,即父程式永遠無法預測子程式到底什麼時候結束,如果子程式一結束就立刻回收其全部資源,那麼在父程式內將無法獲取子程式的狀態資訊。

因此,UNⅨ提供了一種機制可以保證父程式可以在任意時刻獲取子程式結束時的狀態資訊:
1、在每個程式退出的時候,核心釋放該程式所有的資源,包括開啟的檔案,佔用的記憶體等。但是仍然為其保留一定的資訊(包括程式號the process ID,退出狀態the termination status of the process,執行時間the amount of CPU time taken by the process等)
2、直到父程式通過wait / waitpid來取時才釋放. 但這樣就導致了問題,如果程式不呼叫wait / waitpid的話,那麼保留的那段資訊就不會釋放,其程式號就會一直被佔用,但是系統所能使用的程式號是有限的,如果大量的產生僵死程式,將因為沒有可用的程式號而導致系統不能產生新的程式. 此即為殭屍程式的危害,應當避免。

任何一個子程式(init除外)在exit()之後,並非馬上就消失掉,而是留下一個稱為殭屍程式(Zombie)的資料結構,等待父程式處理。這是每個子程式在結束時都要經過的階段。如果子程式在exit()之後,父程式沒有來得及處理,這時用ps命令就能看到子程式的狀態是“Z”。如果父程式能及時 處理,可能用ps命令就來不及看到子程式的殭屍狀態,但這並不等於子程式不經過殭屍狀態。  如果父程式在子程式結束之前退出,則子程式將由init接管。init將會以父程式的身份對殭屍狀態的子程式進行處理。

二:孤兒程式(無害)

孤兒程式:一個父程式退出,而它的一個或多個子程式還在執行,那麼那些子程式將成為孤兒程式。孤兒程式將被init程式(程式號為1)所收養,並由init程式對它們完成狀態收集工作。

孤兒程式是沒有父程式的程式,孤兒程式這個重任就落到了init程式身上,init程式就好像是一個民政局,專門負責處理孤兒程式的善後工作。每當出現一個孤兒程式的時候,核心就把孤 兒程式的父程式設定為init,而init程式會迴圈地wait()它的已經退出的子程式。這樣,當一個孤兒程式淒涼地結束了其生命週期的時候,init程式就會代表黨和政府出面處理它的一切善後工作。因此孤兒程式並不會有什麼危害。

我們來測試一下(建立完子程式後,主程式所在的這個指令碼就退出了,當父程式先於子程式結束時,子程式會被init收養,成為孤兒程式,而非殭屍程式),檔案內容

import os
import sys
import time

pid = os.getpid()
ppid = os.getppid()
print 'im father', 'pid', pid, 'ppid', ppid
pid = os.fork()
# 執行pid=os.fork()則會生成一個子程式
# 返回值pid有兩種值:
# 如果返回的pid值為0,表示在子程式當中
# 如果返回的pid值>0,表示在父程式當中
if pid > 0:
    print 'father died..'
    sys.exit(0)

# 保證主執行緒退出完畢
time.sleep(1)
print 'im child', os.getpid(), os.getppid()

執行檔案,輸出結果:
im father pid 32515 ppid 32015
father died..
im child 32516 1

看,子程式已經被pid為1的init程式接收了,所以殭屍程式在這種情況下是不存在的,存在只有孤兒程式而已,孤兒程式宣告週期結束自然會被init來銷燬。


三:殭屍程式危害場景:

例如有個程式,它定期的產 生一個子程式,這個子程式需要做的事情很少,做完它該做的事情之後就退出了,因此這個子程式的生命週期很短,但是,父程式只管生成新的子程式,至於子程式 退出之後的事情,則一概不聞不問,這樣,系統執行上一段時間之後,系統中就會存在很多的僵死程式,倘若用ps命令檢視的話,就會看到很多狀態為Z的程式。 嚴格地來說,僵死程式並不是問題的根源,罪魁禍首是產生出大量僵死程式的那個父程式。因此,當我們尋求如何消滅系統中大量的僵死程式時,答案就是把產生大 量僵死程式的那個元凶槍斃掉(也就是通過kill傳送SIGTERM或者SIGKILL訊號啦)。槍斃了元凶程式之後,它產生的僵死程式就變成了孤兒進 程,這些孤兒程式會被init程式接管,init程式會wait()這些孤兒程式,釋放它們佔用的系統程式表中的資源,這樣,這些已經僵死的孤兒程式 就能瞑目而去了。

四:測試
# 產生殭屍程式的程式test.py內容如下

#coding:utf-8
from multiprocessing import Process
import time,os

def run():
    print('子',os.getpid())

if __name__ == '__main__':
    p=Process(target=run)
    p.start()

    print('主',os.getpid())
    time.sleep(1000)
                                                     

# 2
等待父程式正常結束後會呼叫wait/waitpid去回收殭屍程式
但如果父程式是一個死迴圈,永遠不會結束,那麼該殭屍程式就會一直存在,殭屍程式過多,就是有害的
解決方法一:殺死父程式
解決方法二:對開啟的子程式應該記得使用join,join會回收殭屍程式
參考python2原始碼註釋
class Process(object):
    def join(self, timeout=None):
        '''
        Wait until child process terminates
        '''
        assert self._parent_pid == os.getpid(), 'can only join a child process'
        assert self._popen is not None, 'can only join a started process'
        res = self._popen.wait(timeout)
        if res is not None:
            _current_process._children.discard(self)

join方法中呼叫了wait,告訴系統釋放殭屍程式。discard為從自己的children中剔除

解決方法三:signal模組(自行了解)

思考:

from multiprocessing import Process
import time,os

def task():
    print('%s is running' %os.getpid())
    time.sleep(3)

if __name__ == '__main__':
    p=Process(target=task)
    p.start()
    p.join() # 等待程式p結束後,join函式內部會傳送系統呼叫wait,去告訴作業系統回收掉程式p的id號

    print(p.pid) # 此時能否看到子程式p的id號
    print('主')

答案

#答案:可以
#分析:
p.join()是像作業系統傳送請求,告知作業系統p的id號不需要再佔用了,回收就可以,
此時在父程式內還可以看到p.pid,但此時的p.pid是一個無意義的id號,因為作業系統已經將該編號回收

打個比方:
我黨相當於作業系統,控制著整個中國的硬體,每個人相當於一個程式,每個人都需要跟我黨申請一個身份證號
該號碼就相當於程式的pid,人死後應該到我黨那裡登出身份證號,p.join()就相當於要求我黨回收身份證號,但p的家人(相當於主程式)
仍然持有p的身份證,但此刻的身份證已經沒有意義

四 守護程式

主程式建立守護程式

其一:守護程式會在主程式程式碼執行結束後就終止

其二:守護程式內無法再開啟子程式,否則丟擲異常:AssertionError: daemonic processes are not allowed to have children

注意:程式之間是互相獨立的,主程式程式碼執行結束,守護程式隨即終止

from multiprocessing import Process
import time
import random

class Play(Process):
    def __init__(self,name):
        self.name=name
        super().__init__()
    def run(self):
        print('%s is playing' %self.name)
        time.sleep(random.randrange(1,3))
        print('%s is play end' %self.name)


p=Play('amgulen')
p.daemon=True # 一定要在p.start()前設定,設定p為守護程式,禁止p建立子程式,並且父程式程式碼執行結束,p即終止執行
p.start()
print('主')

迷惑人的例子

#主程式程式碼執行完畢,守護程式就會結束
from multiprocessing import Process
from threading import Thread
import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")

def bar():
    print(456)
    time.sleep(3)
    print("end456")


p1=Process(target=foo)
p2=Process(target=bar)

p1.daemon=True
p1.start()
p2.start()
print("main-------") # 列印該行則主程式程式碼結束,則守護程式p1應該被終止,可能會有p1任務執行的列印資訊123,因為主程式列印main----時,p1也執行了,但是隨即被終止

五 程式同步(鎖)

程式之間資料不共享,但是共享同一套檔案系統,所以訪問同一個檔案,或同一個列印終端,是沒有問題的,

而共享帶來的是競爭,競爭帶來的結果就是錯亂,如何控制,就是加鎖處理

part1:多個程式共享同一列印終端

併發執行,效率高,但競爭同一列印終端,帶來了列印錯亂

#併發執行,效率高,但競爭同一列印終端,帶來了列印錯亂
from multiprocessing import Process
import os,time
def work():
    print('%s is running' %os.getpid())
    time.sleep(2)
    print('%s is done' %os.getpid())

if __name__ == '__main__':
    for i in range(3):
        p=Process(target=work)
        p.start()

加鎖:由併發變成了序列,犧牲了執行效率,但避免了競爭

#由併發變成了序列,犧牲了執行效率,但避免了競爭
from multiprocessing import Process,Lock
import os,time
def work(lock):
    lock.acquire()
    print('%s is running' %os.getpid())
    time.sleep(2)
    print('%s is done' %os.getpid())
    lock.release()
if __name__ == '__main__':
    lock=Lock()
    for i in range(3):
        p=Process(target=work,args=(lock,))
        p.start()

part2:多個程式共享同一檔案

檔案當資料庫,模擬搶票

併發執行,效率高,但競爭寫同一檔案,資料寫入錯亂

# 檔案db的內容為:{"count":1}
# 注意一定要用雙引號,不然json無法識別
from multiprocessing import Process,Lock
import time,json,random
def search():
    dic=json.load(open('db.txt'))
    print('剩餘票數%s' %dic['count'])

def get():
    dic=json.load(open('db.txt'))
    time.sleep(0.1) # 模擬讀資料的網路延遲
    if dic['count'] >0:
        dic['count']-=1
        time.sleep(0.2) # 模擬寫資料的網路延遲
        json.dump(dic,open('db.txt','w'))
        print('購票成功')

def task(lock):
    search()
    get()
if __name__ == '__main__':
    lock=Lock()
    for i in range(100): # 模擬併發100個客戶端搶票
        p=Process(target=task,args=(lock,))
        p.start()

加鎖:購票行為由併發變成了序列,犧牲了執行效率,但保證了資料安全

# 檔案db的內容為:{"count":1}
# 注意一定要用雙引號,不然json無法識別
from multiprocessing import Process,Lock
import time,json,random
def search():
    dic=json.load(open('db.txt'))
    print('剩餘票數%s' %dic['count'])

def get():
    dic=json.load(open('db.txt'))
    time.sleep(0.1) # 模擬讀資料的網路延遲
    if dic['count'] >0:
        dic['count']-=1
        time.sleep(0.2) # 模擬寫資料的網路延遲
        json.dump(dic,open('db.txt','w'))
        print('購票成功')

def task(lock):
    search()
    lock.acquire()
    get()
    lock.release()
if __name__ == '__main__':
    lock=Lock()
    for i in range(100): # 模擬併發100個客戶端搶票
        p=Process(target=task,args=(lock,))
        p.start()

總結:

#加鎖可以保證多個程式修改同一塊資料時,同一時間只能有一個任務可以進行修改,即序列的修改,沒錯,速度是慢了,但犧牲了速度卻保證了資料安全。
雖然可以用檔案共享資料實現程式間通訊,但問題是:
1.效率低(共享資料基於檔案,而檔案是硬碟上的資料)
2.需要自己加鎖處理

#因此我們最好找尋一種解決方案能夠兼顧:1、效率高(多個程式共享一塊記憶體的資料)2、幫我們處理好鎖問題。這就是mutiprocessing模組為我們提供的基於訊息的IPC通訊機制:佇列和管道。
1 佇列和管道都是將資料存放於記憶體中
2 佇列又是基於(管道+鎖)實現的,可以讓我們從複雜的鎖問題中解脫出來,
我們應該儘量避免使用共享資料,儘可能使用訊息傳遞和佇列,避免處理複雜的同步和鎖問題,而且在程式數目增多時,往往可以獲得更好的可獲展性。

六 佇列(推薦使用)

程式彼此之間互相隔離,要實現程式間通訊(IPC),multiprocessing模組支援兩種形式:佇列和管道,這兩種方式都是使用訊息傳遞的

建立佇列的類(底層就是以管道和鎖定的方式實現)

Queue([maxsize]):建立共享的程式佇列,Queue是多程式安全的佇列,可以使用Queue實現多程式之間的資料傳遞。

引數介紹:

maxsize是佇列中允許最大項數,省略則無大小限制。

方法介紹:

主要方法:

# q.put方法用以插入資料到佇列中,put方法還有兩個可選引數:blocked和timeout。如果blocked為True(預設值),並且timeout為正值,該方法會阻塞timeout指定的時間,直到該佇列有剩餘的空間。如果超時,會丟擲Queue.Full異常。如果blocked為False,但該Queue已滿,會立即丟擲Queue.Full異常。

# q.get方法可以從佇列讀取並且刪除一個元素。同樣,get方法有兩個可選引數:blocked和timeout。如果blocked為True(預設值),並且timeout為正值,那麼在等待時間內沒有取到任何元素,會丟擲Queue.Empty異常。如果blocked為False,有兩種情況存在,如果Queue有一個值可用,則立即返回該值,否則,如果佇列為空,則立即丟擲Queue.Empty異常.

# q.get_nowait():同q.get(False)

# q.put_nowait():同q.put(False)

# q.empty():呼叫此方法時q為空則返回True,該結果不可靠,比如在返回True的過程中,如果佇列中又加入了專案。

# q.full():呼叫此方法時q已滿則返回True,該結果不可靠,比如在返回True的過程中,如果佇列中的專案被取走。

# q.qsize():返回佇列中目前專案的正確數量,結果也不可靠,理由同q.empty()和q.full()一樣

其他方法(瞭解):

# q.cancel_join_thread():不會在程式退出時自動連線後臺執行緒。可以防止join_thread()方法阻塞

# q.close():關閉佇列,防止佇列中加入更多資料。呼叫此方法,後臺執行緒將繼續寫入那些已經入佇列但尚未寫入的資料,但將在此方法完成時馬上關閉。如果q被垃圾收集,將呼叫此方法。關閉佇列不會在佇列使用者中產生任何型別的資料結束訊號或異常。例如,如果某個使用者正在被阻塞在get()操作上,關閉生產者中的佇列不會導致get()方法返回錯誤。

# q.join_thread():連線佇列的後臺執行緒。此方法用於在呼叫q.close()方法之後,等待所有佇列項被消耗。預設情況下,此方法由不是q的原始建立者的所有程式呼叫。呼叫q.cancel_join_thread方法可以禁止這種行為

應用:

'''
multiprocessing模組支援程式間通訊的兩種主要形式:管道和佇列
都是基於訊息傳遞實現的,但是佇列介面
'''

from multiprocessing import Process,Queue
import time
q=Queue(3)


#put ,get ,put_nowait,get_nowait,full,empty
q.put(3)
q.put(3)
q.put(3)
print(q.full()) #滿了

print(q.get())
print(q.get())
print(q.get())
print(q.empty()) #空了

生產者消費者模型

在併發程式設計中使用生產者和消費者模式能夠解決絕大多數併發問題。該模式通過平衡生產執行緒和消費執行緒的工作能力來提高程式的整體處理資料的速度。

為什麼要使用生產者和消費者模式

線上程世界裡,生產者就是生產資料的執行緒,消費者就是消費資料的執行緒。在多執行緒開發當中,如果生產者處理速度很快,而消費者處理速度很慢,那麼生產者就必須等待消費者處理完,才能繼續生產資料。同樣的道理,如果消費者的處理能力大於生產者,那麼消費者就必須等待生產者。為了解決這個問題於是引入了生產者和消費者模式。

什麼是生產者消費者模式

生產者消費者模式是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通訊,而通過阻塞佇列來進行通訊,所以生產者生產完資料之後不用等待消費者處理,直接扔給阻塞佇列,消費者不找生產者要資料,而是直接從阻塞佇列裡取,阻塞佇列就相當於一個緩衝區,平衡了生產者和消費者的處理能力。

基於佇列實現生產者消費者模型

from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))

def producer(q):
    for i in range(10):
        time.sleep(random.randint(1,3))
        res='包子%s' %i
        q.put(res)
        print('\033[44m%s 生產了 %s\033[0m' %(os.getpid(),res))

if __name__ == '__main__':
    q=Queue()
    # 生產者們:即廚師們
    p1=Process(target=producer,args=(q,))

    # 消費者們:即吃貨們
    c1=Process(target=consumer,args=(q,))

    # 開始
    p1.start()
    c1.start()
    print('主')

生產者消費者模型總結

# 生產者消費者模型總結
# 程式中有兩類角色
一類負責生產資料(生產者)
一類負責處理資料(消費者)
#引入生產者消費者模型為了解決的問題是:
平衡生產者與消費者之間的工作能力,從而提高程式整體處理資料的速度
#如何實現:
生產者<-->佇列<——>消費者
#生產者消費者模型實現類程式的解耦和

此時的問題是主程式永遠不會結束,原因是:生產者p在生產完後就結束了,但是消費者c在取空了q之後,則一直處於死迴圈中且卡在q.get()這一步。

解決方式無非是讓生產者在生產完畢後,往佇列中再發一個結束訊號,這樣消費者在接收到結束訊號後就可以break出死迴圈

生產者在生產完畢後傳送結束訊號None

from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        if res is None:break # 到結束訊號則結束
        time.sleep(random.randint(1,3))
        print('%s 吃 %s' %(os.getpid(),res))

def producer(q):
    for i in range(10):
        time.sleep(random.randint(1,3))
        res='包子%s' %i
        q.put(res)
        print('%s 生產了 %s' %(os.getpid(),res))
    q.put(None) # 傳送結束訊號
if __name__ == '__main__':
    q=Queue()
    # 生產者們:即廚師們
    p1=Process(target=producer,args=(q,))

    # 消費者們:即吃貨們
    c1=Process(target=consumer,args=(q,))

    # 開始
    p1.start()
    c1.start()
    print('主')

注意:結束訊號None,不一定要由生產者發,主程式裡同樣可以發,但主程式需要等生產者結束後才應該傳送該訊號

主程式在生產者生產完畢後傳送結束訊號None

from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        if res is None:break # 收到結束訊號則結束
        time.sleep(random.randint(1,3))
        print('%s 吃 %s' %(os.getpid(),res))

def producer(q):
    for i in range(2):
        time.sleep(random.randint(1,3))
        res='包子%s' %i
        q.put(res)
        print('%s 生產了 %s' %(os.getpid(),res))

if __name__ == '__main__':
    q=Queue()
    # 生產者們:即廚師們
    p1=Process(target=producer,args=(q,))

    # 消費者們:即吃貨們
    c1=Process(target=consumer,args=(q,))

    # 開始
    p1.start()
    c1.start()

    p1.join()
    q.put(None) # 傳送結束訊號
    print('主')

但上述解決方式,在有多個生產者和多個消費者時,我們則需要用一個很low的方式去解決

有幾個消費者就需要傳送幾次結束訊號:相當low

from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        if res is None:break # 收到結束訊號則結束
        time.sleep(random.randint(1,3))
        print('%s 吃 %s' %(os.getpid(),res))

def producer(name,q):
    for i in range(2):
        time.sleep(random.randint(1,3))
        res='%s%s' %(name,i)
        q.put(res)
        print('%s 生產了 %s' %(os.getpid(),res))



if __name__ == '__main__':
    q=Queue()
    # 生產者們:即廚師們
    p1=Process(target=producer,args=('包子',q))
    p2=Process(target=producer,args=('骨頭',q))
    p3=Process(target=producer,args=('泔水',q))

    # 消費者們:即吃貨們
    c1=Process(target=consumer,args=(q,))
    c2=Process(target=consumer,args=(q,))

    # 開始
    p1.start()
    p2.start()
    p3.start()
    c1.start()

    p1.join() # 必須保證生產者全部生產完畢,才應該傳送結束訊號
    p2.join()
    p3.join()
    q.put(None) # 有幾個消費者就應該傳送幾次結束訊號None
    q.put(None) # 傳送結束訊號
    print('主')

其實我們的思路無非是傳送結束訊號而已,有另外一種佇列提供了這種機制

# JoinableQueue([maxsize]):這就像是一個Queue物件,但佇列允許專案的使用者通知生成者專案已經被成功處理。通知程式是使用共享的訊號和條件變數來實現的。

# 引數介紹:
    maxsize是佇列中允許最大項數,省略則無大小限制。    
# 方法介紹:
    JoinableQueue的例項p除了與Queue物件相同的方法之外還具有:
    q.task_done():使用者使用此方法發出訊號,表示q.get()的返回專案已經被處理。如果呼叫此方法的次數大於從佇列中刪除專案的數量,將引發ValueError異常
    q.join():生產者呼叫此方法進行阻塞,直到佇列中所有的專案均被處理。阻塞將持續到佇列中的每個專案均呼叫q.task_done()方法為止
from multiprocessing import Process,JoinableQueue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        time.sleep(random.randint(1,3))
        print('%s 吃 %s' %(os.getpid(),res))

        q.task_done() # 向q.join()傳送一次訊號,證明一個資料已經被取走了

def producer(name,q):
    for i in range(10):
        time.sleep(random.randint(1,3))
        res='%s%s' %(name,i)
        q.put(res)
        print('%s 生產了 %s' %(os.getpid(),res))
    q.join()


if __name__ == '__main__':
    q=JoinableQueue()
    # 生產者們:即廚師們
    p1=Process(target=producer,args=('包子',q))
    p2=Process(target=producer,args=('骨頭',q))
    p3=Process(target=producer,args=('泔水',q))

    # 消費者們:即吃貨們
    c1=Process(target=consumer,args=(q,))
    c2=Process(target=consumer,args=(q,))
    c1.daemon=True
    c2.daemon=True

    # 開始
    p_l=[p1,p2,p3,c1,c2]
    for p in p_l:
        p.start()

    p1.join()
    p2.join()
    p3.join()
    print('主') 

    # 主程式等--->p1,p2,p3等---->c1,c2
    # p1,p2,p3結束了,證明c1,c2肯定全都收完了p1,p2,p3發到佇列的資料
    # 因而c1,c2也沒有存在的價值了,應該隨著主程式的結束而結束,所以設定成守護程式

七 管道

程式間通訊(IPC)方式二:管道(不推薦使用,瞭解即可)

介紹

# 建立管道的類:
Pipe([duplex]):在程式之間建立一條管道,並返回元組(conn1,conn2),其中conn1,conn2表示管道兩端的連線物件,強調一點:必須在產生Process物件之前產生管道
# 引數介紹:
dumplex:預設管道是全雙工的,如果將duplex射成False,conn1只能用於接收,conn2只能用於傳送。
# 主要方法:
    conn1.recv():接收conn2.send(obj)傳送的物件。如果沒有訊息可接收,recv方法會一直阻塞。如果連線的另外一端已經關閉,那麼recv方法會丟擲EOFError。
    conn1.send(obj):通過連線傳送物件。obj是與序列化相容的任意物件
# 其他方法:
conn1.close():關閉連線。如果conn1被垃圾回收,將自動呼叫此方法
conn1.fileno():返回連線使用的整數檔案描述符
conn1.poll([timeout]):如果連線上的資料可用,返回True。timeout指定等待的最長時限。如果省略此引數,方法將立即返回結果。如果將timeout射成None,操作將無限期地等待資料到達。

conn1.recv_bytes([maxlength]):接收c.send_bytes()方法傳送的一條完整的位元組訊息。maxlength指定要接收的最大位元組數。如果進入的訊息,超過了這個最大值,將引發IOError異常,並且在連線上無法進行進一步讀取。如果連線的另外一端已經關閉,再也不存在任何資料,將引發EOFError異常。
conn.send_bytes(buffer [, offset [, size]]):通過連線傳送位元組資料緩衝區,buffer是支援緩衝區介面的任意物件,offset是緩衝區中的位元組偏移量,而size是要傳送位元組數。結果資料以單條訊息的形式發出,然後呼叫c.recv_bytes()函式進行接收    

conn1.recv_bytes_into(buffer [, offset]):接收一條完整的位元組訊息,並把它儲存在buffer物件中,該物件支援可寫入的緩衝區介面(即bytearray物件或類似的物件)。offset指定緩衝區中放置訊息處的位元組位移。返回值是收到的位元組數。如果訊息長度大於可用的緩衝區空間,將引發BufferTooShort異常。

基於管道實現程式間通訊(與佇列的方式是類似的,佇列就是管道加鎖實現的)

from multiprocessing import Process,Pipe

import time,os
def consumer(p,name):
    left,right=p
    left.close()
    while True:
        try:
            baozi=right.recv()
            print('%s 收到包子:%s' %(name,baozi))
        except EOFError:
            right.close()
            break
def producer(seq,p):
    left,right=p
    right.close()
    for i in seq:
        left.send(i)
        # time.sleep(1)
    else:
        left.close()
if __name__ == '__main__':
    left,right=Pipe()
    c1=Process(target=consumer,args=((left,right),'c1'))
    c1.start()
    seq=(i for i in range(10))
    producer(seq,(left,right))
    right.close()
    left.close()
    c1.join()
    print('主程式')

注意:生產者和消費者都沒有使用管道的某個端點,就應該將其關閉,如在生產者中關閉管道的右端,在消費者中關閉管道的左端。如果忘記執行這些步驟,程式可能再消費者中的recv()操作上掛起。管道是由作業系統進行引用計數的,必須在所有程式中關閉管道後才能生產EOFError異常。因此在生產者中關閉管道不會有任何效果,付費消費者中也關閉了相同的管道端點。

管道可以用於雙向通訊,利用通常在客戶端/伺服器中使用的請求/響應模型或遠端過程呼叫,就可以使用管道編寫與程式互動的程式

from multiprocessing import Process,Pipe

import time,os
def adder(p,name):
    server,client=p
    client.close()
    while True:
        try:
            x,y=server.recv()
        except EOFError:
            server.close()
            break
        res=x+y
        server.send(res)
    print('server done')
if __name__ == '__main__':
    server,client=Pipe()

    c1=Process(target=adder,args=((server,client),'c1'))
    c1.start()

    server.close()

    client.send((10,20))
    print(client.recv())
    client.close()

    c1.join()
    print('主程式')
# 注意:send()和recv()方法使用pickle模組對物件進行序列化。

八 共享資料

展望未來,基於訊息傳遞的併發程式設計是大勢所趨

即便是使用執行緒,推薦做法也是將程式設計為大量獨立的執行緒集合

通過訊息佇列交換資料。這樣極大地減少了對使用鎖定和其他同步手段的需求,

還可以擴充套件到分散式系統中

程式間通訊應該儘量避免使用本節所講的共享資料的方式

程式間資料是獨立的,可以藉助於佇列或管道實現通訊,二者都是基於訊息傳遞的

雖然程式間資料獨立,但可以通過Manager實現資料共享,事實上Manager的功能遠不止於此

A manager object returned by Manager() controls a server process which holds Python objects and allows other processes to manipulate them using proxies.

A manager returned by Manager() will support types list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array. For example,

程式之間操作共享的資料

from multiprocessing import Manager,Process,Lock
import os
def work(d,lock):
    # with lock: #不加鎖而操作共享的資料,肯定會出現資料錯亂
        d['count']-=1

if __name__ == '__main__':
    lock=Lock()
    with Manager() as m:
        dic=m.dict({'count':100})
        p_l=[]
        for i in range(100):
            p=Process(target=work,args=(dic,lock))
            p_l.append(p)
            p.start()
        for p in p_l:
            p.join()
        print(dic)
        # {'count': 94}

九 訊號量(瞭解)

訊號量Semahpore(同執行緒一樣)

互斥鎖 同時只允許一個執行緒更改資料,而Semaphore是同時允許一定數量的執行緒更改資料 ,比如廁所有3個坑,那最多隻允許3個人上廁所,後面的人只能等裡面有人出來了才能再進去,如果指定訊號量為3,那麼來一個人獲得一把鎖,計數加1,當計數等於3時,後面的人均需要等待。一旦釋放,就有人可以獲得一把鎖

訊號量與程式池的概念很像,但是要區分開,訊號量涉及到加鎖的概念

from multiprocessing import Process,Semaphore
import time,random

def go_wc(sem,user):
    sem.acquire()
    print('%s 佔到一個茅坑' %user)
    time.sleep(random.randint(0,3)) # 模擬每個人拉屎速度不一樣,0代表有的人蹲下就起來了
    sem.release()

if __name__ == '__main__':
    sem=Semaphore(5)
    p_l=[]
    for i in range(13):
        p=Process(target=go_wc,args=(sem,'user%s' %i,))
        p.start()
        p_l.append(p)

    for i in p_l:
        i.join()
    print('============》')

十 事件(瞭解)

Event(同執行緒一樣)

python執行緒的事件用於主執行緒控制其他執行緒的執行,事件主要提供了三個方法 set、wait、clear。

事件處理的機制:全域性定義了一個“Flag”,如果“Flag”值為 False,那麼當程式執行 event.wait 方法時就會阻塞,如果“Flag”值為True,那麼event.wait 方法時便不再阻塞。

clear:將“Flag”設定為False
set:將“Flag”設定為True

from multiprocessing import Process,Event
import time,random

def car(e,n):
    while True:
        if not e.is_set(): #Flase
            print('\033[31m紅燈亮\033[0m,car%s等著' %n)
            e.wait()
            print('\033[32m車%s 看見綠燈亮了\033[0m' %n)
            time.sleep(random.randint(3,6))
            if not e.is_set():
                continue
            print('走你,car', n)
            break

def police_car(e,n):
    while True:
        if not e.is_set():
            print('\033[31m紅燈亮\033[0m,car%s等著' % n)
            e.wait(1)
            print('燈的是%s,警車走了,car %s' %(e.is_set(),n))
            break

def traffic_lights(e,inverval):
    while True:
        time.sleep(inverval)
        if e.is_set():
            e.clear() # e.is_set() ---->False
        else:
            e.set()

if __name__ == '__main__':
    e=Event()
    # for i in range(10):
    # p=Process(target=car,args=(e,i,))
    # p.start()

    for i in range(5):
        p = Process(target=police_car, args=(e, i,))
        p.start()
    t=Process(target=traffic_lights,args=(e,10))
    t.start()

    print('============》')

十一 程式池

在利用Python進行系統管理的時候,特別是同時操作多個檔案目錄,或者遠端控制多臺主機,並行操作可以節約大量的時間。多程式是實現併發的手段之一,需要注意的問題是:

  1. 很明顯需要併發執行的任務通常要遠大於核數
  2. 一個作業系統不可能無限開啟程式,通常有幾個核就開幾個程式
  3. 程式開啟過多,效率反而會下降(開啟程式是需要佔用系統資源的,而且開啟多餘核數目的程式也無法做到並行)

例如當被操作物件數目不大時,可以直接利用multiprocessing中的Process動態成生多個程式,十幾個還好,但如果是上百個,上千個。手動的去限制程式數量卻又太過繁瑣,此時可以發揮程式池的功效。

我們就可以通過維護一個程式池來控制程式數目,比如httpd的程式模式,規定最小程式數和最大程式數

ps:對於遠端過程呼叫的高階應用程式而言,應該使用程式池,Pool可以提供指定數量的程式,供使用者呼叫,當有新的請求提交到pool中時,如果池還沒有滿,那麼就會建立一個新的程式用來執行該請求;但如果池中的程式數已經達到規定最大值,那麼該請求就會等待,直到池中有程式結束,就重用程式池中的程式。

建立程式池的類:如果指定numprocess為3,則程式池會從無到有建立三個程式,然後自始至終使用這三個程式去執行所有任務,不會開啟其他程式

Pool([numprocess  [,initializer [, initargs]]]):建立程式池

引數介紹:

numprocess:要建立的程式數,如果省略,將預設使用cpu_count()的值
initializer:是每個工作程式啟動時要執行的可呼叫物件,預設為None
initargs:是要傳給initializer的引數組

方法介紹:

主要方法:

# p.apply(func [, args [, kwargs]]):在一個池工作程式中執行func(*args,**kwargs),然後返回結果。需要強調的是:此操作並不會在所有池工作程式中並執行func函式。如果要通過不同引數併發地執行func函式,必須從不同執行緒呼叫p.apply()函式或者使用p.apply_async()

# p.apply_async(func [, args [, kwargs]]):在一個池工作程式中執行func(*args,**kwargs),然後返回結果。此方法的結果是AsyncResult類的例項,callback是可呼叫物件,接收輸入引數。當func的結果變為可用時,將理解傳遞給callback。callback禁止執行任何阻塞操作,否則將接收其他非同步操作中的結果。
   
# p.close():關閉程式池,防止進一步操作。如果所有操作持續掛起,它們將在工作程式終止前完成

# P.jion():等待所有工作程式退出。此方法只能在close()或teminate()之後呼叫

其他方法(瞭解部分)

方法apply_async()和map_async()的返回值是AsyncResul的例項obj。例項具有以下方法
obj.get():返回結果,如果有必要則等待結果到達。timeout是可選的。如果在指定時間內還沒有到達,將引發一場。如果遠端操作中引發了異常,它將在呼叫此方法時再次被引發。
obj.ready():如果呼叫完成,返回True
obj.successful():如果呼叫完成且沒有引發異常,返回True,如果在結果就緒之前呼叫此方法,引發異常
obj.wait([timeout]):等待結果變為可用。
obj.terminate():立即終止所有工作程式,同時不執行任何清理或結束任何掛起工作。如果p被垃圾回收,將自動呼叫此函式

應用:

同步呼叫apply

from multiprocessing import Pool
import os,time
def work(n):
    print('%s run' %os.getpid())
    time.sleep(3)
    return n**2

if __name__ == '__main__':
    p=Pool(3) # 程式池中從無到有建立三個程式,以後一直是這三個程式在執行任務
    res_l=[]
    for i in range(10):
        res=p.apply(work,args=(i,)) # 同步呼叫,直到本次任務執行完畢拿到res,等待任務work執行的過程中可能有阻塞也可能沒有阻塞,但不管該任務是否存在阻塞,同步呼叫都會在原地等著,只是等的過程中若是任務發生了阻塞就會被奪走cpu的執行許可權
        res_l.append(res)
    print(res_l)

非同步呼叫apply_async

from multiprocessing import Pool
import os,time
def work(n):
    print('%s run' %os.getpid())
    time.sleep(3)
    return n**2

if __name__ == '__main__':
    p=Pool(3) # 程式池中從無到有建立三個程式,以後一直是這三個程式在執行任務
    res_l=[]
    for i in range(10):
        res=p.apply_async(work,args=(i,)) # 同步執行,阻塞、直到本次任務執行完畢拿到res
        res_l.append(res)

    # 非同步apply_async用法:如果使用非同步提交的任務,主程式需要使用jion,等待程式池內任務都處理完,然後可以用get收集結果,否則,主程式結束,程式池可能還沒來得及執行,也就跟著一起結束了
    p.close()
    p.join()
    for res in res_l:
        print(res.get()) # 使用get來獲取apply_aync的結果,如果是apply,則沒有get方法,因為apply是同步執行,立刻獲取結果,也根本無需get

詳解:apply_async與apply

# 一:使用程式池(非同步呼叫,apply_async)
# coding: utf-8
from multiprocessing import Process,Pool
import time

def func(msg):
    print( "msg:", msg)
    time.sleep(1)
    return msg

if __name__ == "__main__":
    pool = Pool(processes = 3)
    res_l=[]
    for i in range(10):
        msg = "hello %d" %(i)
        res=pool.apply_async(func, (msg, ))   # 維持執行的程式總數為processes,當一個程式執行完畢後會新增新的程式進去
        res_l.append(res)
    print("==============================>") # 沒有後面的join,或get,則程式整體結束,程式池中的任務還沒來得及全部執行完也都跟著主程式一起結束了

    pool.close() # 關閉程式池,防止進一步操作。如果所有操作持續掛起,它們將在工作程式終止前完成
    pool.join()   # 呼叫join之前,先呼叫close函式,否則會出錯。執行完close後不會有新的程式加入到pool,join函式等待所有子程式結束

    print(res_l) # 看到的是<multiprocessing.pool.ApplyResult object at 0x10357c4e0>物件組成的列表,而非最終的結果,但這一步是在join後執行的,證明結果已經計算完畢,剩下的事情就是呼叫每個物件下的get方法去獲取結果
    for i in res_l:
        print(i.get()) # 使用get來獲取apply_aync的結果,如果是apply,則沒有get方法,因為apply是同步執行,立刻獲取結果,也根本無需get

# 二:使用程式池(同步呼叫,apply)
# coding: utf-8
from multiprocessing import Process,Pool
import time

def func(msg):
    print( "msg:", msg)
    time.sleep(0.1)
    return msg

if __name__ == "__main__":
    pool = Pool(processes = 3)
    res_l=[]
    for i in range(10):
        msg = "hello %d" %(i)
        res=pool.apply(func, (msg, ))   # 維持執行的程式總數為processes,當一個程式執行完畢後會新增新的程式進去
        res_l.append(res) # 同步執行,即執行完一個拿到結果,再去執行另外一個
    print("==============================>")
    pool.close()
    pool.join()   # 呼叫join之前,先呼叫close函式,否則會出錯。執行完close後不會有新的程式加入到pool,join函式等待所有子程式結束

    print(res_l) # 看到的就是最終的結果組成的列表
    for i in res_l: # apply是同步的,所以直接得到結果,沒有get()方法
        print(i)

練習2:使用程式池維護固定數目的程式(重寫練習1)

server端

# Pool內的程式數預設是cpu核數,假設為4(檢視方法os.cpu_count())
# 開啟6個客戶端,會發現2個客戶端處於等待狀態
# 在每個程式內檢視pid,會發現pid使用為4個,即多個客戶端公用4個程式
from socket import *
from multiprocessing import Pool
import os

server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
server.bind(('127.0.0.1',8080))
server.listen(5)

def talk(conn,client_addr):
    print('程式pid: %s' %os.getpid())
    while True:
        try:
            msg=conn.recv(1024)
            if not msg:break
            conn.send(msg.upper())
        except Exception:
            break

if __name__ == '__main__':
    p=Pool()
    while True:
        conn,client_addr=server.accept()
        p.apply_async(talk,args=(conn,client_addr))
        # p.apply(talk,args=(conn,client_addr)) #同步的話,則同一時間只有一個客戶端能訪問

客戶端

from socket import *

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))


while True:
    msg=input('>>: ').strip()
    if not msg:continue

    client.send(msg.encode('utf-8'))
    msg=client.recv(1024)
    print(msg.decode('utf-8'))

發現:併發開啟多個客戶端,服務端同一時間只有3個不同的pid,幹掉一個客戶端,另外一個客戶端才會進來,被3個程式之一處理

回掉函式:

需要回撥函式的場景:程式池中任何一個任務一旦處理完了,就立即告知主程式:我好了額,你可以處理我的結果了。主程式則呼叫一個函式去處理該結果,該函式即回撥函式

我們可以把耗時間(阻塞)的任務放到程式池中,然後指定回撥函式(主程式負責執行),這樣主程式在執行回撥函式時就省去了I/O的過程,直接拿到的是任務的結果。

from multiprocessing import Pool
import requests
import json
import os

def get_page(url):
    print('<程式%s> get %s' %(os.getpid(),url))
    respone=requests.get(url)
    if respone.status_code == 200:
        return {'url':url,'text':respone.text}

def pasrse_page(res):
    print('<程式%s> parse %s' %(os.getpid(),res['url']))
    parse_res='url:<%s> size:[%s]\n' %(res['url'],len(res['text']))
    with open('db.txt','a') as f:
        f.write(parse_res)


if __name__ == '__main__':
    urls=[
        'https://www.baidu.com',
        'https://www.python.org',
        'https://www.openstack.org',
        'https://help.github.com/',
        'http://www.sina.com.cn/'
    ]

    p=Pool(3)
    res_l=[]
    for url in urls:
        res=p.apply_async(get_page,args=(url,),callback=pasrse_page)
        res_l.append(res)

    p.close()
    p.join()
    print([res.get() for res in res_l]) #拿到的是get_page的結果,其實完全沒必要拿該結果,該結果已經傳給回撥函式處理了

'''
列印結果:
<程式3388> get https://www.baidu.com
<程式3389> get https://www.python.org
<程式3390> get https://www.openstack.org
<程式3388> get https://help.github.com/
<程式3387> parse https://www.baidu.com
<程式3389> get http://www.sina.com.cn/
<程式3387> parse https://www.python.org
<程式3387> parse https://help.github.com/
<程式3387> parse http://www.sina.com.cn/
<程式3387> parse https://www.openstack.org
[{'url': 'https://www.baidu.com', 'text': '<!DOCTYPE html>\r\n...',...}]
'''

爬蟲案例

from multiprocessing import Pool
import time,random
import requests
import re

def get_page(url,pattern):
    response=requests.get(url)
    if response.status_code == 200:
        return (response.text,pattern)

def parse_page(info):
    page_content,pattern=info
    res=re.findall(pattern,page_content)
    for item in res:
        dic={
            'index':item[0],
            'title':item[1],
            'actor':item[2].strip()[3:],
            'time':item[3][5:],
            'score':item[4]+item[5]

        }
        print(dic)
if __name__ == '__main__':
    pattern1=re.compile(r'<dd>.*?board-index.*?>(\d+)<.*?title="(.*?)".*?star.*?>(.*?)<.*?releasetime.*?>(.*?)<.*?integer.*?>(.*?)<.*?fraction.*?>(.*?)<',re.S)

    url_dic={
        'http://maoyan.com/board/7':pattern1,
    }

    p=Pool()
    res_l=[]
    for url,pattern in url_dic.items():
        res=p.apply_async(get_page,args=(url,pattern),callback=parse_page)
        res_l.append(res)

    for i in res_l:
        i.get()

    # res=requests.get('http://maoyan.com/board/7')
    # print(re.findall(pattern,res.text))

如果在主程式中等待程式池中所有任務都執行完畢後,再統一處理結果,則無需回撥函式

from multiprocessing import Pool
import time,random,os

def work(n):
    time.sleep(1)
    return n**2
if __name__ == '__main__':
    p=Pool()

    res_l=[]
    for i in range(10):
        res=p.apply_async(work,args=(i,))
        res_l.append(res)

    p.close()
    p.join() # 等待程式池中所有程式執行完畢

    nums=[]
    for res in res_l:
        nums.append(res.get()) # 拿到所有結果
    print(nums) # 主程式拿到所有的處理結果,可以在主程式中進行統一進行處理

程式池的其他實現方式:https://docs.python.org/dev/library/concurrent.futures.html

相關文章