Python併發編程(三):多進程(實戰提高篇)

一、進程同步(互斥鎖)

進程之間數據不共享,但是共享同一套文件系統,所以訪問同一個文件,或同一個打印終端,是沒有問題的,

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

1、實戰(文件當數據庫,模擬搶票)

# 未加鎖
#文件db的內容爲:{"count":1}
#注意一定要用雙引號,不然json無法識別
from multiprocessing import Process
import time,json
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():
    search()
    get()
if __name__ == '__main__':
    for i in range(100): #模擬併發100個客戶端搶票
        p=Process(target=task)
        p.start()
# 加鎖:購票行爲由併發變成了串行,犧牲了運行效率,但保證了數據安全
#文件db的內容爲:{"count":1}
#注意一定要用雙引號,不然json無法識別
from multiprocessing import Process,Lock
import time,json,random
def search():
    dic=json.load(open('db.txt'))
    print('\033[43m剩餘票數%s\033[0m' %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('\033[43m購票成功\033[0m')

def task(lock):
    search()
    lock.acquire() # 拿到鎖,100個進程會競爭獲取鎖
    get()
    lock.release() # 釋放鎖
if __name__ == '__main__':
    lock=Lock()
    for i in range(100): # 模擬併發100個客戶端搶票
        p=Process(target=task,args=(lock,))
        p.start()

2、總結

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


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

二、管道(不推薦)

1、基礎知識

進程彼此之間互相隔離,要實現進程間通信(IPC),multiprocessing模塊支持兩種形式:隊列和管道,這兩種方式都是使用消息傳遞的。管道不推薦使用,瞭解即可。

用途:

  • 在linux中的ps -ef|grep nginx|grep -v grep這種命令就採用了管道,把一個命令的輸出通過管道傳輸給第二個命令,作爲它的輸入!
  • subprocess模塊中就採用了管道
#創建管道的類:
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異常。

2、基於管道實現進程間通信

from multiprocessing import Process,Pipe

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關閉
        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異常。因此在生產者中關閉管道不會有任何效果,消費者中也關閉了相同的管道端點。

三、共享數據(不推薦)

展望未來,基於消息傳遞的併發編程是大勢所趨

即便是使用線程,推薦做法也是將程序設計爲大量獨立的線程集合

通過消息隊列交換數據。這樣極大地減少了對使用鎖和其他同步手段的需求,

還可以擴展到分佈式系統中

進程間通信還有一種共享的方式,進程間通信應該儘量避免使用本節所講的共享數據的方式

進程間數據是獨立的,可以藉助於隊列或管道實現通信,二者都是基於消息傳遞的

雖然進程間數據獨立,但可以通過Manager實現數據共享,事實上Manager的功能遠不止於此
# 進程之間操作共享的數據
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}

四、隊列(推薦)

1、創建隊列的類(底層就是以管道+鎖的方式實現)

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方法可以禁止這種行爲

2、應用

'''
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()) #空了

五、生產者消費者模型

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

1、爲什麼使用生產者和消費者模式

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

2、什麼是生產者消費者模式

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

3、基於隊列實現生產者消費者模型

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出死循環

但是記住有幾個消費者就需要發送幾次結束信號,不然不能將所有進程都停下來。這個方法相當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('\033[45m%s 喫 %s\033[0m' %(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('\033[44m%s 生產了 %s\033[0m' %(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('主')

4、生產者消費者模型總結

#生產者消費者模型總結

    #程序中有兩類角色
        一類負責生產數據(生產者)
        一類負責處理數據(消費者)

    #引入生產者消費者模型爲了解決的問題是:
        平衡生產者與消費者之間的工作能力,從而提高程序整體處理數據的速度

    #如何實現:
        生產者<-->隊列<——>消費者
    #生產者消費者模型實現類程序的解耦和

5、JoinableQueue隊列

上面我們採用了發送結束信號,消費者進程一看接收到的是結束信號,就break掉不斷從隊列取食物的循環。但是這種方法太麻煩且太低級,我們的JoinableQueue隊列就是專門用於這種場景。

#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('\033[45m%s 喫 %s\033[0m' %(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('\033[44m%s 生產了 %s\033[0m' %(os.getpid(),res))
    q.join() # 這是隊列的join,上面for循環生產完食物,就會阻塞在這裏,直到q.task_done取完隊列裏面的數據


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() # 這是進程的join用於父子進程的同步。等子進程執行完了,才往下執行,不要與隊列的join混淆
    p2.join()
    p3.join()
    print('主') 

    #主進程等--->p1,p2,p3等---->c1,c2
    # q.join加上p1.join確保了p1生產完了食物,且p1的食物全被消費者進程喫完了才退出來的,因此p1、p2、p3三個join方法執行完,說明c1、c2任務也完成了!
    # 因而c1,c2也沒有存在的價值了,應該隨着主進程的結束而結束,所以設置成守護進程

總結:

1. 當生產者進程向隊列put添加數據時,JoinableQueue隊列內部有個計數器會自動加1
2. 消費者進程從隊列中取數據時,執行q.task_done()時,內部的計數器自動減1
3. q.join會阻塞進程,直到計數器爲0,才繼續往下執行!

六、信號量(互斥鎖池,瞭解)

注意實際並沒有互斥鎖池這個概念的存在,只是博主爲了方便大家理解說的。

信號量我感覺也有點池的感覺在裏面,也就是說它和進程池有點像。只不過進程池這個池裏面裝的是一定數量的進程,而信號量這個池子裏面裝的一定數量的互斥鎖!進程池在覈數能夠支持的情況下,可以實現一定數量的進程並行執行的效果,因爲信號量有多把鎖,因此信號量實現多個進程可以同時獲得鎖,同時對數據進行處理,需要謹慎使用,不然會造成數據污染!

# 互斥鎖 同時只允許一個線程更改數據,而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('============》')

七、事件(主進程控制其他子進程,瞭解)

python進程的事件用於主進程控制其他進程的執行
事件處理的機制:全局定義了一個內置標誌Flag,如果Flag值爲 False,那麼當程序執行 event.wait方法時就會阻塞,如果Flag值爲True,那麼event.wait 方法時便不再阻塞。

# Event類的對象的主要方法
1. set(): 將標誌設爲True,並通知所有處於等待阻塞狀態的線程恢復運行狀態。

2. clear(): 將標誌設爲False3. wait(timeout): 如果標誌爲True將立即返回,否則阻塞線程至等待阻塞狀態,等待其他線程調用set()4. is_set(): 獲取內置標誌狀態,返回TrueFalse
from multiprocessing import Event, Process
import time
import random
def light(e):
    while True:
        if e.is_set(): # True,不阻塞,是綠燈,是綠燈,兩秒過去就應該轉換成紅燈了
            e.clear()
            print('\033[31m紅燈亮了\033[0m')
        else:
            e.set()  # False,阻塞,是紅燈,兩秒過去就應該轉換成綠燈了
            print('\033[32m綠燈亮了\033[0m')
        time.sleep(2) # 模擬紅綠燈轉換的間隙時間

def cars(e,i):
    if not e.is_set(): # False,會阻塞,表示'紅燈亮了:':
        print('car%i在等待'%i)
        e.wait() #'等紅燈'阻塞直到得到一個事件狀態變爲True的信號,然後執行下面打印通過的信息
    #'車通行'
    print('\033[0;32;40mcar%i通過\033[0m'%i)

if __name__ == '__main__':
    e = Event()
    traffic = Process(target=light,args=(e,))
    traffic.start()
    for i in range(20):
        car = Process(target=cars, args=(e,i))
        car.start()
        time.sleep((random.random()))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章