併發程式設計(二)

HammerZe發表於2022-01-18

併發程式設計(二)

殭屍程式

檢視源影像

正常:程式程式碼執行結束之後並沒有直接結束而是需要等待回收子程式資源才能結束;

殭屍程式是當子程式比父程式先結束,而父程式又沒有回收子程式,釋放子程式佔用的資源,此時子程式將成為一個殭屍程式。. 如果父程式先退出 ,子程式被init接管,子程式退出後init會回收其佔用的相關資源;

通俗理解:如果我們出去露營,在回家的時候是不是需要把擺出來的行李收拾回去才可以呀,不能人嗨了東西不要了;

from multiprocessing import Process
import time

def process_test(name):
    print(f'{name} is running!')
    time.sleep(2)
    print(f'{name} is over!')

if __name__ == '__main__':
    p = Process(target=process_test,args=('Hammer',))
    p.start()
    print('主執行緒')

# 主程式執行了print輸出語句已經沒有程式碼可以執行了,但是子程式是在該程式內開設的,需要等待回收子程式的資源

image


孤兒程式

即主程式已經死亡(非正常)但是子程式還在執行

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

通俗的理解:“沒爹沒媽”

image

這樣是浪費資源的,沒有主程式回收子程式,最後會由作業系統linit回收;


守護程式

守護程式:即守護著某個程式 一旦這個程式結束那麼也隨之結束;

通俗理解:“程式陪葬”

from multiprocessing import Process
import time


def test(name):
    print('總管:%s is running' % name)
    time.sleep(3)
    print('總管:%s is over' % name)


if __name__ == '__main__':
    p = Process(target=test, args=('Hammer',))
    p.daemon = True  # 設定為守護程式(一定要放在start語句上方)
    p.start()
    print("皇帝壽終正寢")
# 只有主程式的print語句,而子程式的輸出語句不會執行
# 當主程式結束了,子程式也隨即結束,就相當於皇帝死了,太監們就別玩了,而是去陪葬

注意:p.daemon = True在啟動子程式前執行!


互斥鎖

問題:併發情況下操作同一份資料 極其容易造成資料錯亂

解決措施:將併發變成序列 雖然降低了效率但是提升了資料的安全

鎖(行鎖、表鎖···)就可以實現將併發變成序列的效果

'''data.txt'''
{"ticket_num":1}

'''buy.py'''
import json
from multiprocessing import Process, Lock
import time
import random


# 查票
def search(name):
    with open(r'data.txt', 'r', encoding='utf8') as f:
        data_dict = json.load(f)  # 反序列化
        ticket_num = data_dict.get('ticket_num')
        print('%s查詢餘票:%s' % (name, ticket_num))


# 買票
def buy(name):
    # 先查票
    with open(r'data.txt', 'r', encoding='utf8') as f:
        data_dict = json.load(f)
        ticket_num = data_dict.get('ticket_num')
    # 模擬一個延遲
    time.sleep(random.random())
    # 判斷是否有票
    if ticket_num > 0:
        # 將餘票減一
        data_dict['ticket_num'] -= 1
        # 重新寫入資料庫
        with open(r'data.txt', 'w', encoding='utf8') as f:
            json.dump(data_dict, f)
        print('%s: 購買成功' % name)
    else:
        print('不好意思 沒有票了!!!')


def run(name,mutex):
    search(name)
    mutex.acquire()  # 搶鎖
    buy(name)
    mutex.release()  # 釋放鎖
# 只需將買票上鎖

# 開設多個程式
if __name__ == '__main__':
    mutex = Lock()  # 多程式如果不上鎖就會出現資料錯亂
    for i in range(1, 11):
        p = Process(target=run, args=('使用者%s' % i,mutex))
        p.start()
        
# 搶鎖的概念就相當於十個人去搶一個茅坑,一個人拉上了,後面的人只能等著了,拉完了(釋放鎖),別人才能繼續搶鎖;
        
# 在以後的程式設計生涯中 幾乎不會解除到自己操作鎖的情況

樂觀鎖與悲觀鎖

樂觀鎖

  • 總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號機制和CAS演算法實現。樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,像資料庫提供的類似於write_condition機制,其實都是提供的樂觀鎖;
  • 樂觀鎖:假設不會發生併發衝突,只在提交操作時檢查是否違反資料完整性;
  • 樂觀鎖:多數用於資料爭用不大、衝突較少的環境中,這種環境中,偶爾回滾事務的成本會低於讀取資料時鎖定資料的成本,因此可以獲得比其他併發控制方法更高的吞吐量。
    相對於悲觀鎖,在對資料庫進行處理的時候,樂觀鎖並不會使用資料庫提供的鎖機制。一般的實現樂觀鎖的方式就是記錄資料版本;

悲觀鎖

  • 總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖(共享資源每次只給一個執行緒使用,其它執行緒阻塞,用完後再把資源轉讓給其它執行緒,和Python中GIL鎖一樣)。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖;

  • 悲觀鎖:假定會發生併發衝突,遮蔽一切可能違反資料完整性的操作;

  • 悲觀鎖:主要用於資料爭用激烈的環境,以及發生併發衝突時使用鎖保護資料的成本要低於回滾事務的成本的環境中;

參考:https://blog.csdn.net/qq_34337272/article/details/81072874

訊息佇列

佇列:先進先出

方法示例寫在程式碼中

from multiprocessing import Queue

# 括號內可以填寫最大的等待數,佇列最多裝5個數
q = Queue(5)

# put方法,存放資料
q.put(111)
q.put(222)
print(q.full())  # 判斷佇列是否放滿
q.put(333)
q.put(444)
q.put(555)
print(q.full())  # 佇列放滿了

# 超出範圍原地等待,直到有空缺位置
# q.put(666)

# 提取資料
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())

# 只有五個數,取六個的時候,原地等待,直到有資料過來
# print(q.get())

# 判斷資料是否為空
print(q.empty())
# 獲取資料,沒有資料立刻報錯
print(q.get_nowait())  # queue.Empty

注意:

  • fullget_nowait不能用於多程式情況下的精確使用如果另外程式從當前程式拿資料,或者存入資料,會造成資料的偏差,從而full方法get_nowait方法判斷就會出錯;

  • 佇列的使用就可以打破程式間預設無法通訊的情況,這樣就相當於開拓了一塊公共空間(訊息佇列),每個程式都可以從中拿或存資料;

  • [訊息佇列推薦博文](訊息佇列(mq)是什麼? - 知乎 (zhihu.com))


IPC機制

佇列的使用就可以打破程式間預設無法通訊的情況,IPC機制來實現

image

# 主程式和子程式通過訊息佇列來實現資料互通

# IPC機制來解決程式間無法通訊的問題
from multiprocessing import Queue,Process


def consumer(q):
    print(q.get())  # 子程式獲取主程式存放的資料
    q.put("子程式存放的資料")   # 模擬開設的子程式中存放的資料

if __name__ == '__main__':
    q = Queue()  # 生成佇列
    q.put("主程式存放的資料") # 模擬資料
    # 開設子程式
    p = Process(target=consumer,args=(q,))
    p.start()
    p.join()  # 用join方法來確認子程式執行完,主程式再去取資料
    print(q.get())  # 主程式獲取
    print("主程式")
# 子程式和子程式通過訊息佇列來實現資料互通
from multiprocessing import Queue,Process

def process1(q):
    q.put("子程式1存放的資料!")


def process2(q):
    print(f'子程式2取子程式1存放的資料:>>>{q.get()}')

if __name__ == '__main__':
    q = Queue()
    p1 = Process(target=process1,args=(q,))
    p2 = Process(target=process2,args=(q,))
    p1.start()
    p2.start()

生產者消費者模型

?[23種設計模型參考博文]((3條訊息) Python之23種設計模式_Burgess_zheng的部落格-CSDN部落格_python 設計模式)

  • 生產者:負責產生資料
  • 消費者:負責處理資料

爬蟲領域用的比較多,該模型需要解決供需不平衡的現象

# 使用到模組JoinableQueue,能夠監測誰給佇列中放資料和取資料
from multiprocessing import Queue, Process, JoinableQueue
import time
import random


def producer(name, food, q):
    for i in range(10):
        print('%s 生產了 %s' % (name, food))
        q.put(food)
        time.sleep(random.random())


def consumer(name, q):
    while True:
        data = q.get()
        print('%s 吃了 %s' % (name, data))
        q.task_done()


if __name__ == '__main__':
    # q = Queue()
    q = JoinableQueue()   # 佇列模組換一下
    p1 = Process(target=producer, args=('廚子Hammer', '瑪莎拉', q))
    p2 = Process(target=producer, args=('印度阿三', '飛餅', q))
    p3 = Process(target=producer, args=('泰國阿人', '榴蓮', q))
    c1 = Process(target=consumer, args=('班長阿飛', q))

    p1.start()
    p2.start()
    p3.start()
    c1.daemon = True   # 守護程式
    c1.start()

    p1.join()
    p2.join()
    p3.join()

    q.join()  # 等待佇列中所有的資料被取乾淨

    print('主程式')
    
# 使用模組JoinableQueue並且新增守護程式和join方法是為了讓消費者結束等待的情況

相關文章