1. 程式人生 > 實用技巧 >(三)多程序之守護程序與互斥鎖

(三)多程序之守護程序與互斥鎖

一、守護程序


1,主程序建立子程序,然後將該程序設定成守護自己的程序,守護程序就好比皇帝身邊的老太監,皇帝已死老太監就跟著殉葬了。

關於守護程序需要強調兩點:

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

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

如果我們有兩個任務需要併發執行,那麼開一個主程序和一個子程序分別去執行就ok了,如果子程序的任務在主程序任務結束後就沒有存在的必要了,那麼該子程序應該在開啟前就被設定成守護程序。主程序程式碼執行結束,守護程序隨即終止。

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

from multiprocessing import Process
import time

def task(name):
    print("%s is running." % name)
    time.sleep(2)

if __name__ == "__main__":
    p = Process(target=task,args=("子程序1",))
    p.daemon = True     # 守護程序一定是要在開啟之前設定,設定p為守護程序,禁止p建立子程序,並且父程序程式碼執行結束,p即終止執行。
p.start() print("") # 只要終端打印出這行內容,主程序就算執行完了,那麼守護程序 p 也就跟著結束掉了。

2,練習題:思考下列程式碼的執行結果有可能有哪些情況?why?

from multiprocessing import Process
import time

def foo():
    print(123)
    time.sleep(1)
    print("end123")

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

if __name__
== '__main__': p1 = Process(target=foo) p2 = Process(target=bar) p1.daemon = True p1.start() p2.start() print("main-------")
from multiprocessing import Process
import time

def foo():
    print(123)
    time.sleep(1)
    print("end123")

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

if __name__ == '__main__':
    p1 = Process(target=foo)
    p2 = Process(target=bar)
    p1.daemon = True  # 把 p1 設定成守護程序,main------ 打印出來之後,它就跟著沒了

    p1.start()
    p2.start()
    print("main-------")    # 這行打印出來,主程序就執行完了

# 機器效能高的情況下,會出現,p2 的訊號的傳送過程中,p1 已經立馬開啟起來了,會瞬間列印123,然後會列印main----,然後p2起來
# 但是隻要看到main------,絕對不會看到 end123。

"""
main-------
456
end456
"""
result

二、互斥鎖


程序之間資料不共享,但是共享同一套檔案系統,所以訪問同一個檔案,或同一個列印終端,是沒有問題的,而共享帶來的是競爭,競爭帶來的結果就是錯亂,如下:

# 併發執行,效率高,但競爭同一列印終端,帶來了列印錯亂。
from multiprocessing import Process
import time

def task(name):
    print("%s 1" % name)
    time.sleep(1)
    print("%s 2" % name)
    time.sleep(1)
    print("%s 3" % name)


if __name__ == "__main__":
    for i in range(3):
        p = Process(target=task,args=("程序%s" % i,))
        p.start()


"""
結果是錯亂,三個程序是共享同一個輸出終端的,
共享帶來的就是競爭,誰搶到了,誰就列印,這就用到了互斥鎖。

程序0 1
程序2 1
程序1 1
程序0 2
程序1 2
程序2 2
程序0 3
程序1 3
程序2 3
"""

如何控制,就是加鎖處理。而互斥鎖的意思就是互相排斥,如果把多個程序比喻為多個人,互斥鎖的工作原理就是多個人都要去爭搶同一個資源:衛生間,一個人搶到衛生間後上一把鎖,其他人都要等著,等到這個完成任務後釋放鎖,其他人才有可能有一個搶到......所以互斥鎖的原理,就是把併發改成序列,降低了效率,但保證了資料安全不錯亂。

# 由併發變成了序列,犧牲了執行效率,但避免了競爭
from multiprocessing import Process,Lock
import time

def task(name,mutex):
    mutex.acquire()     # 加鎖

    print("%s 1" % name)
    time.sleep(1)
    print("%s 2" % name)
    time.sleep(1)
    print("%s 3" % name)

    mutex.release()     # 把鎖釋放掉

if __name__ == "__main__":
    mutex = Lock()
    for i in range(3):
        p = Process(target=task,args=("程序%s" % i,mutex))    # 確保所有的程序用的是同一把鎖,就把這個鎖傳遞給子程序。
        p.start()


"""
雖然效率降低了,但是避免了競爭,錯亂

程序0 1
程序0 2
程序0 3
程序1 1
程序1 2
程序1 3
程序2 1
程序2 2
程序2 3
"""

三、模擬搶票練習


多個程序共享同一檔案,我們可以把檔案當資料庫,用多個程序模擬多個人執行搶票任務。

# 檔案db.txt的內容為:{"count":1}
# 注意一定要用雙引號,不然json無法識別
from multiprocessing import Process
import json
import time

def search(name):
    """查票"""
    time.sleep(1)   # 模擬網路延遲
    dic = json.load(open("db.txt","r",encoding="utf-8"))
    print("<%s>檢視到剩餘票數<%s>" % (name,dic["count"]))


def get(name):
    """購票"""
    time.sleep(1)
    dic = json.load(open("db.txt", "r", encoding="utf-8"))
    if dic["count"] > 0:
        dic["count"] -= 1
        time.sleep(3)
        json.dump(dic,open("db.txt","w",encoding="utf-8"))
        print("<%s>購票成功" % name)

def task(name):
    search(name)
    get(name)

if __name__ == "__main__":
    for i in range(1,11):
        p = Process(target=task,args=("路人%s" % i,))
        p.start()

# 併發執行,效率高,但競爭同一檔案,資料寫入錯亂,只有一張票,卻成功賣給了10個人

"""
<路人1>檢視到剩餘票數<1>
<路人2>檢視到剩餘票數<1>
<路人3>檢視到剩餘票數<1>
<路人4>檢視到剩餘票數<1>
<路人5>檢視到剩餘票數<1>
<路人6>檢視到剩餘票數<1>
<路人7>檢視到剩餘票數<1>
<路人8>檢視到剩餘票數<1>
<路人9>檢視到剩餘票數<1>
<路人10>檢視到剩餘票數<1>
<路人1>購票成功
<路人2>購票成功
<路人3>購票成功
<路人4>購票成功
<路人5>購票成功
<路人6>購票成功
<路人7>購票成功
<路人8>購票成功
<路人9>購票成功
<路人10>購票成功
"""
併發執行,效率高,但競爭同一檔案,資料寫入錯亂,只有一張票,賣成功給了10個人。
# 加鎖處理:購票行為由併發變成了序列,犧牲了執行效率,但保證了資料安全
from multiprocessing import Process,Lock
import json
import time

def search(name):
    """查票"""
    time.sleep(1)   # 模擬網路延遲
    dic = json.load(open("db.txt","r",encoding="utf-8"))
    print("<%s>檢視到剩餘票數<%s>" % (name,dic["count"]))


def get(name):
    """購票"""
    time.sleep(1)
    dic = json.load(open("db.txt", "r", encoding="utf-8"))
    if dic["count"] > 0:
        dic["count"] -= 1
        time.sleep(3)
        json.dump(dic,open("db.txt","w",encoding="utf-8"))
        print("<%s>購票成功" % name)

def task(name,mutex):
    search(name)

    mutex.acquire()
    get(name)
    mutex.release()

if __name__ == "__main__":
    mutex = Lock()
    for i in range(1,11):
        p = Process(target=task,args=("路人%s" % i,mutex))
        p.start()

"""
<路人1>檢視到剩餘票數<1>
<路人2>檢視到剩餘票數<1>
<路人3>檢視到剩餘票數<1>
<路人4>檢視到剩餘票數<1>
<路人5>檢視到剩餘票數<1>
<路人6>檢視到剩餘票數<1>
<路人7>檢視到剩餘票數<1>
<路人8>檢視到剩餘票數<1>
<路人9>檢視到剩餘票數<1>
<路人10>檢視到剩餘票數<1>
<路人1>購票成功

Process finished with exit code 0
"""
加鎖處理:購票行為由併發變成了序列,犧牲了執行效率,但保證了資料的安全

四、互斥鎖與join


使用 join 可以將併發變成序列,互斥鎖的原理也是將併發變成穿行,那我們直接使用 join 就可以了啊,為何還要互斥鎖,那我們就來試一下:

# 互斥鎖與 join
from multiprocessing import Process
import json
import time

def search(name):
    """查票"""
    time.sleep(1)   # 模擬網路延遲
    dic = json.load(open("db.txt","r",encoding="utf-8"))
    print("<%s>檢視到剩餘票數<%s>" % (name,dic["count"]))


def get(name):
    """購票"""
    time.sleep(1)
    dic = json.load(open("db.txt", "r", encoding="utf-8"))
    if dic["count"] > 0:
        dic["count"] -= 1
        time.sleep(3)
        json.dump(dic,open("db.txt","w",encoding="utf-8"))
        print("<%s>購票成功" % name)
    else:
        print("<%s>購票失敗" % name)

def task(name):
    search(name)
    get(name)

if __name__ == "__main__":
    for i in range(1,11):
        p = Process(target=task,args=("路人%s" % i,))
        p.start()
        p.join()    # 程序按順序一個個執行

"""
這種方式連檢視票都變成序列的了,而我們只想要購票是序列的。
<路人1>檢視到剩餘票數<1>
<路人1>購票成功
<路人2>檢視到剩餘票數<0>
<路人2>購票失敗
<路人3>檢視到剩餘票數<0>
<路人3>購票失敗
<路人4>檢視到剩餘票數<0>
<路人4>購票失敗
<路人5>檢視到剩餘票數<0>
<路人5>購票失敗
<路人6>檢視到剩餘票數<0>
<路人6>購票失敗
<路人7>檢視到剩餘票數<0>
<路人7>購票失敗
<路人8>檢視到剩餘票數<0>
<路人8>購票失敗
<路人9>檢視到剩餘票數<0>
<路人9>購票失敗
<路人10>檢視到剩餘票數<0>
<路人10>購票失敗
"""

發現使用 join 將併發改成穿行,確實能保證資料安全,但問題是連查票操作也變成只能一個一個人去查了,很明顯大家查票時應該是併發地去查詢而無需考慮資料準確與否,此時 join 與互斥鎖的區別就顯而易見了,join是將一個任務整體序列,而互斥鎖的好處則是可以將一個任務中的某一段程式碼序列,比如只讓 task 函式中的 get 任務序列:

def task(name,):
    search(name)    # 併發執行

    mutex.acquire()
    get(name)   # 序列執行
    mutex.release()

總結:

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



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

Within daemon can no longer turn a child process