python 並發編程

來源:互聯網
上載者:User

標籤:task   情況下   win   pre   並發編程   檔案系統   als   官方   經理   

多進程和多線程的守護區別

首先明確一點,無論是多進程還是多線程,主進程或主線程都會等待子進程或子線程退出才會退出。
無論是進程還是線程,都遵循:守護xxx會等待主xxx運行完畢後被銷毀. 需要強調的是:運行完畢並非終止運行
1.對主進程來說,運行完畢指的是主進程代碼運行完畢
2.對主線程來說,運行完畢指的是主線程所在的進程內所有非守護線程統統運行完畢,主線程才算運行完畢
也就是說:

  1. 主進程在其代碼結束後就已經算運行完畢了(守護進程在此時就被回收),然後主進程會一直等非守護的子進程都運行完畢後回收子進程的資源(否則會產生殭屍進程),才會結束,
    2 . 主線程在其他非守護線程運行完畢後才算運行完畢(守護線程在此時就被回收)。因為主線程的結束意味著進程的結束,進程整體的資源都將被回收,而進程必須保證非守護線程都運行完畢後才能結束。
import osimport timefrom multiprocessing import Processdef task1():    while True:        print('task1', os.getpid())        time.sleep(1)def task2():    while True:        print('task2')        time.sleep(1.5)if __name__ == '__main__':    p1 = Process(target=task1)    p1.daemon = True    p1.start()    p2 = Process(target=task2)    p2.start()    # task1 不會被執行,因為進程的開啟是比線程慢的,所以一般情況下是主進程代碼執行完畢再執行子進程    print('main over')
GIL 鎖

GIL鎖是一把cpython解譯器幫我們加的互斥鎖,加這把鎖是基於記憶體管理機制考慮的。如果沒有這把鎖,再考慮多個線程同時只能被一個CPU處理的情況,因為沒有鎖的多線程的並發肯定會設計到資源搶佔。記憶體回收機制的活還沒幹完就被另外一個線程搶走了CPU的執行許可權,恰好這個線程又要對記憶體回收要處理的一個變數做相關操作(比如加1),那這種情況記憶體回收就沒有意義了。所以,在進程空間的外面加了一把鎖,如果記憶體回收機制搶到這把鎖,把記憶體回收的活幹完釋放鎖,這樣記憶體回收機制才能實現。

GIL鎖和我們自己在程式申明的鎖有什麼區別?GIL鎖可以看做是整個進程空間出口上的鎖,而我們自己申明的鎖是用來鎖住進程裡面的的資料的。

多進程的鎖

多進程是資料隔離的,為啥還需要鎖呢?因為多進程雖然是資料隔離,但是卻共用檔案系統和列印終端。如果是開啟多個進程隊檔案進程讀寫操作,那麼就需要用到鎖

from multiprocessing import Process,Lockimport time,jsondef search():    dic=json.load(open('ticket.txt'))    print('\033[43m剩餘票數%s\033[0m' %dic['count'])def get():    dic=json.load(open('ticket.txt'))    time.sleep(0.1) # 類比讀資料的網路延遲,這裡是為了等待其他進程開啟並完成load操作    if dic['count'] > 0:        dic['count'] -= 1        time.sleep(0.2) # 類比寫資料的網路延遲,以防dump的w模式清空檔案但是另一個進程在search的時候load空檔案會報錯        json.dump(dic,open('ticket.txt','w'))        print('\033[43m購票成功\033[0m')def task(lock):    search()    get()if __name__ == '__main__':    lock=Lock()    for i in range(100): #類比並發100個用戶端搶票        p=Process(target=task,args=(lock,))        p.start()

加鎖

from multiprocessing import Process,Lockimport time,jsonfrom multiprocessing import Lockdef search():    dic=json.load(open('ticket.txt'))    print('\033[43m剩餘票數%s\033[0m' %dic['count'])def get():    dic=json.load(open('ticket.txt'))    time.sleep(0.1) # 類比讀資料的網路延遲,這裡是為了等待其他進程開啟並完成load操作    if dic['count'] > 0:        dic['count'] -= 1        time.sleep(0.2) # 類比寫資料的網路延遲,以防dump的w模式清空檔案但是另一個進程在search的時候load空檔案會報錯        json.dump(dic,open('ticket.txt','w'))        print('\033[43m購票成功\033[0m')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()

這裡不是在get函數裡面加鎖,而是另外封裝了一個task函數,在task函數裡面給get() 加鎖

隊列

無論是進程還是線程的隊列,都已經幫我們做過加鎖處理了,而且隊列裡可以丟進去自訂的對象,還可以丟None對象。線程可以共用全域變數,進程雖然不能共用全域變數,可以有可以共用資料的方式。同線程共用
資料就會有搶佔資源的情況,進程如果使用共用資料的方式,也會出現資源競爭的情況。multiprocessing的Manager其實就是另外開一個進程,在這個進程裡面開闢一塊共用記憶體。

池multiprocessing的Pool

同步使用方式(不常用)

from multiprocessing import Poolimport os,timedef work(n):    print('%s run' %os.getpid())    time.sleep(3)    return n**2if __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)

非同步

from multiprocessing import Poolimport os,timedef work(n):    time.sleep(1)    return n**2if __name__ == '__main__':    p=Pool(os.cpu_count()) #進程池中從無到有建立三個進程,以後一直是這三個進程在執行任務    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()    # join之後get() 就能立即拿到值,如果注釋掉上面兩句代碼,那麼是每3個列印一次,沒有結果的get會出現阻塞    for res in res_l:        print(res.get()) #使用get來擷取apply_aync的結果,如果是apply,則沒有get方法,因為apply是同步執行,立刻擷取結果,也根本無需get

回調

from multiprocessing import Poolimport os,timedef work(n):    time.sleep(1)    return n**2def get_data(data):    # 會把work return的結果傳給get_data 做參數    print('得到的資料是:', data)if __name__ == '__main__':    p=Pool(os.cpu_count()) #進程池中從無到有建立三個進程,以後一直是這三個進程在執行任務    res_l=[]    # 回呼函數是主進程執行的,如果主進程調用time.sleep(10000),當work執行完畢之後    # 作業系統就會跟主進程說,別睡了,快去處理任務,處理完再睡    for i in range(10):        p.apply_async(work,args=(i,), callback=get_data) #同步運行,阻塞、直到本次任務執行完畢拿到res    # 因為是非同步,如果不加下面兩句,那麼主進程退出,池子裡的任務還沒執行    p.close()    p.join()

對結果的處理方式一般就兩種: 一種是拿到結果立即調用,另外一個是把結果拿到做統一處理,推薦使用第一種方式

concurrent
import timefrom concurrent.futures import ProcessPoolExecutordef work(n):    time.sleep(1)    return n**2def get_data(res):    # 傳的參數res是一個對象    print('得到的資料是:', res.result())if __name__ == '__main__':    executor = ProcessPoolExecutor(max_workers=5)    # futures = []    # for i in range(10):    #     future = executor.submit(work, i).add_done_callback(get_data)    #     futures.append(future)    # executor.shutdown()    # for future in futures:    #     print(future.result())    for i in range(10):        executor.submit(work, i).add_done_callback(get_data)    # 下面不加shutdown也能執行池裡的任務,只不過為了代碼可讀性,一般還是建議加上

map使用方式

from concurrent.futures import ProcessPoolExecutorimport os,time,randomdef task(n):    print('%s is runing' %os.getpid())    time.sleep(random.randint(1,3))    return n**2if __name__ == '__main__':    executor = ProcessPoolExecutor(max_workers=3)    # for i in range(11):    #     # concurrent 模組不加shutdown 主進程執行完畢會等池裡的任務執行完畢程式才會結束,不同於Pool    #     # 而且 concurrent 是用非同步,沒有Pool的同步方式    #     executor.submit(task,i)    # map 得到的是一個儲存結果(不需要.result()) 的可迭代對象    data_obj = executor.map(task,range(1,12)) # map取代了for+submit    for data in data_obj:        print(data)
greenlet 和 gevent

python中的協程就是greenlet,就是切換+儲存狀態。線程是作業系統調度的,但是協程的切換是程式員進行調度的,作業系統對此“不可見”。既然要儲存狀態,那麼肯定就會涉及到棧,協程也有自己的棧,只不過這個開銷比線程小。單純的協程並不能幫我們提高效率,只能幫我們儲存上次啟動並執行狀態並做來回切換,所以大家又基於協程的切換+儲存狀態做了進一步的封裝,讓程式能遇到IO阻塞自動切換,如gevent模組等。gevent模組就是利用事件驅動庫 libev 加 greenlet實現的。我們知道,事件迴圈是非同步編程的底層基石。如果使用者關注的層次很低,直接操作epoll去構造維護事件的迴圈,從底層到高層的商務邏輯需要層層回調,造成callback hell,並且可讀性較差。所以,這個繁瑣的註冊回調與回調的過程得以封裝,並抽象成EventLoop。EventLoop屏蔽了進行epoll系統調用的具體操作。對於使用者來說,將不同的I/O狀態考量為事件的觸發,只需關注更高層次下不同事件的回調行為。諸如libev, libevent之類的使用C編寫的高效能非同步事件庫已經取代這部分瑣碎的工作。綜上所述,當事件發生的時候通知使用者程式進行協程的切換。準確來說,gevent是一個第三方非同步模組,這個模組能讓我像使用線程的方式去使用協程,而且這個模組需要socket是非阻塞socket的,所以一般在程式最開頭加上monkey.patch_all().

import greenletdef task1():    print('task1 start')    g2.switch()    print('task1 end')    g2.switch()def task2():    print('task2 start')    g1.switch()    print('task2 end')g1 = greenlet.greenlet(task1)g2 = greenlet.greenlet(task2)g1.switch()

gevent模組的基本使用

from gevent import spawn, joinall, monkeyimport timemonkey.patch_all()def task(i):    time.sleep(1)    print('----', i)    return i * 2if __name__ == '__main__':    # spawn 的時候會建立一個協程並立即執行    res = [spawn(task, i) for i in range(10)]    joinall(res)    for g in res:        print(g.value)
IO 模型使用者空間和核心空間,使用者態和核心態

現在作業系統都是採用虛擬儲存空間,那麼對32位作業系統而言,它的定址空間(虛擬儲存空間)為4G(2的32次方)。作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。為了保證使用者進程不能直接操作核心(kernel),保證核心的安全,操心系統將虛擬空間劃分為兩部分,一部分為核心空間,一部分為使用者空間。針對linux作業系統而言,將最高的1G位元組(從虛擬位址0xC0000000到0xFFFFFFFF),供核心使用,稱為核心空間,而將較低的3G位元組(從虛擬位址0x00000000到0xBFFFFFFF),供各個進程使用,稱為使用者空間。

使用者態和核心態講述的是CPU的狀態,使用者態是CPU可以執行的指令比較少,核心態是CPU可以處理的指令比較多,可以先這麼簡單地理解。

同步,非同步,阻塞和非阻塞

同步和非同步,阻塞和非阻塞這兩組的關注點是不同的。
同步是程式員的程式"親自"主動去等結果,當然在這個等結果的過程中程式可以幹別的事情(進程或線程的狀態是非阻塞),但是程式還得過一段時間來查看是否已經有結果了,程式需要幹兩件事,只有一個角色
非同步是程式員的程式發了個口號去要結果,然後就等另外一個"東西"通知我結果已經好了,這時候角色就有兩個了。當然,你可以在這個過程去幹別的事,也可以不幹(除非你傻)。在這裡的訊息通知也可以看作是回調,
還記得上面主進程在sleep的時候當work的任務完成之後作業系統會把主進程叫醒執行回呼函數嗎?
阻塞和非阻塞描述的是進程或者線程所處的狀態。

幾種不同的IO模型
  • 阻塞IO
  • 非阻塞IO
  • IO多工(又名事件驅動)
  • 非同步IO

上面的四種不同的IO模型是針對兩個階段的不同狀態而言的:1. 等待資料到核心空間 2. 把資料從核心空間拷貝到使用者程式的進程空間

1、輸入操作:read、readv、recv、recvfrom、recvmsg共5個函數,如果會阻塞狀態,則會經理wait data和copy data兩個階段,如果設定為非阻塞則在wait 不到data時拋出異常2、輸出操作:write、writev、send、sendto、sendmsg共5個函數,在發送緩衝區滿了會阻塞在原地,如果設定為非阻塞,則會拋出異常3、接收外來連結:accept,與輸入操作類似4、發起外出連結:connect,與輸出操作類似

對IO多工補充:

  1. 如果處理的串連數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server效能更好,可能延遲還更大。select/epoll的優勢並不是對於單個串連能處理得更快,而是在於能處理更多的串連。
  2. 在多工模型中,對於每一個socket,一般都設定成為non-blocking(由我們自己的程式不停地while迴圈去詢問socket資料是否準備好改成由作業系統幫我們做這事,所以一般socket設定成non-blocking),但是,如所示,整個使用者的process其實是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。
select、poll和epool

這三種技術都是"古人"開發的,是作業系統層級的程式開發。

  1. select: 效率最低,但有最大描述符限制,在linux為1024,多種平台都支援。作業系統是通過迴圈一遍監聽的檔案對象列表看是否有對象的狀態發生改變,如果是列表第一個和最後一個狀態發生改變,需要從前到後迴圈才能得知這兩個發生改變。
  2. poll: 和select一樣,但沒有最大描述符限制。
  3. epoll: 效率最高,沒有最大描述符限制,支援水平觸發與邊緣觸發。windows系統不支援。epoll監聽的socket發生變化會通過回調來通知作業系統。

IO多工中的兩種觸發方式:

  1. 水平觸發:如果檔案描述符已經就緒可以非阻塞的執行IO操作了,此時會觸發通知.允許在任意時刻重複資料偵測IO的狀態, 沒有必要每次描述符就緒後儘可能多的執行IO.select,poll就屬於水平觸發。
  2. 邊緣觸發:如果檔案描述符自上次狀態改變後有新的IO活動到來,此時會觸發通知.在收到一個IO事件通知後要儘可能 多的執行IO操作,因為如果在一次通知中沒有執行完IO那麼就需要等到下一次新的IO活動到來才能擷取到就緒的描述 符.訊號驅動式IO就屬於邊緣觸發。

    selectors 模組
from socket import *import selectorssel=selectors.DefaultSelector()def accept(server_fileobj,mask):    conn,addr=server_fileobj.accept()    sel.register(conn,selectors.EVENT_READ,read)def read(conn,mask):    try:        data=conn.recv(1024)        if not data:            print('closing',conn)            sel.unregister(conn)            conn.close()            return        conn.send(data.upper()+b'_SB')    except Exception:        print('closing', conn)        sel.unregister(conn)        conn.close()server_fileobj=socket(AF_INET,SOCK_STREAM)server_fileobj.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)server_fileobj.bind(('127.0.0.1',8088))server_fileobj.listen(5)server_fileobj.setblocking(False) #設定socket的介面為非阻塞sel.register(server_fileobj,selectors.EVENT_READ,accept) #相當於網select的讀列表裡append了一個檔案控制代碼server_fileobj,並且綁定了一個回呼函數acceptwhile True:    events=sel.select() #檢測所有的fileobj,是否有完成wait data的    for sel_obj,mask in events:        callback=sel_obj.data #callback=accpet        callback(sel_obj.fileobj,mask) #accpet(server_fileobj,1)#用戶端from socket import *c=socket(AF_INET,SOCK_STREAM)c.connect(('127.0.0.1',8088))while True:    msg=input('>>: ')    if not msg:continue    c.send(msg.encode('utf-8'))    data=c.recv(1024)    print(data.decode('utf-8'))
線程死結的問題解決方案把原來多把互斥鎖改成一把可重複鎖

互斥鎖只能acquire一次,可重複鎖可以acquire多次,這裡的acquire多次是針對單個線程而言的,如果A線程acquire了,在A線程內還能再次acquire(內部維護了一個計數器),但是其他線程就不能acquire

多把互斥鎖按照順序去擷取

每個線程按照固定的順序去擷取鎖就不會出問題,比如要想擷取鎖,必須遵循先A鎖後B鎖的順序,那麼一個線程擷取A鎖,那麼另外的線程要想擷取鎖的時候也必須先拿A鎖,以此來解決死結問題。

import threadingimport timefrom contextlib import contextmanager_local = threading.local()# 統一介面,以後想要擷取鎖就用這個函數去擷取,這個函數把鎖做了排序@contextmanagerdef acquire(*locks):    locks = sorted(locks, key=lambda x: id(x))    # 這裡用threading.local() 儲存線程局部變數,主要用於嵌套去擷取鎖的情況    # 記錄線程已經擷取的鎖,然後和要擷取的鎖的id做比較,順序不符合就拋出異常    acquired_locks = getattr(_local, 'acquired', [])    if acquired_locks and max(id(lock) for lock in acquired_locks) >= id(locks[0]):        raise RuntimeError('出現死結了,程式報錯退出')    acquired_locks.extend(locks)    _local.acquired = acquired_locks    try:        for lock in locks:            lock.acquire()        yield    finally:        for lock in reversed(locks):            lock.release()        del acquired_locks[-len(locks):]if __name__ == '__main__':    x_lock = threading.Lock()    y_lock = threading.Lock()    # def thread_1():    #     with acquire(x_lock, y_lock):    #         print('Thread-1')    #    # def thread_2():    #     with acquire(y_lock, x_lock):    #         print('Thread-2')    # t1 = threading.Thread(target=thread_1)    # t1.start()    # t2 = threading.Thread(target=thread_2)    # t2.start()    def thread_1():        with acquire(x_lock):            with acquire(y_lock):                print('Thread-1')    def thread_2():        with acquire(y_lock):            with acquire(x_lock):                print('Thread-2')    t1 = threading.Thread(target=thread_1)    t1.start()    t2 = threading.Thread(target=thread_2)    t2.start()
python 中的非同步究竟是怎麼實現的

掌握python非同步起點是:epoll + callback + 事件迴圈

epoll

判斷非阻塞調用是否就緒如果 OS 能做,是不是應用程式就可以不用自己去等待和判斷了,就可以利用這個空閑去做其他事情以提高效率。
所以OS將I/O狀態的變化都封裝成了事件,如可讀事件、可寫事件。並且提供了專門的系統模組讓應用程式可以接收事件通知。這個模組就是select。讓應用程式可以通過select註冊檔案描述符和回呼函數。當檔案描述符的狀態發生變化時,select 就調用事先註冊的回呼函數。
select因其演算法效率比較低,後來改進成了poll,再後來又有進一步改進,BSD核心改進成了kqueue模組,而Linux核心改進成了epoll模組。這四個模組的作用都相同,暴露給程式員使用的API也幾乎一致,區別在於kqueue 和 epoll 在處理大量檔案描述符時效率更高。
Python標準庫提供的selectors模組是對底層select/poll/epoll/kqueue的封裝。DefaultSelector類會根據 OS 環境自動選擇最佳的模組,那在 Linux 2.5.44 及更新的版本上都是epoll了

回調(Callback)

把I/O事件的等待和監聽任務交給了 OS,那 OS 在知道I/O狀態發生改變後(例如socket串連已建立成功可發送資料),它又怎麼知道接下來該幹嘛呢?只能回調。
需要我們將發送資料與讀取資料封裝成獨立的函數,讓epoll代替應用程式監聽socket狀態時,得告訴epoll:“如果socket狀態變為可以往裡寫資料(串連建立成功了),請調用HTTP請求發送函數。如果socket 變為可以讀資料了(用戶端已收到響應),請調用響應處理函數。這裡的回調是通知使用者進程去做,而不是作業系統去執行回呼函數。

事件迴圈

這個迴圈是我們程式員在程式裡寫的迴圈,不是作業系統的epoll的迴圈,我們通過這個迴圈,去訪問selector模組,等待它告訴我們當前是哪個事件發生了,應該對應哪個回調。這個等待事件通知的迴圈,稱之為事件迴圈。

selector.select() 是一個阻塞調用,因為如果事件不發生,那應用程式就沒事件可處理,所以就乾脆阻塞在這裡等待事件發生。那可以推斷,如果只下載一篇網頁,一定要connect()之後才能send()繼而recv(),那它的效率和阻塞的方式是一樣的。因為不在connect()/recv()上阻塞,也得在select()上阻塞。所以,selector機制是設計用來解決大量並發串連的。當系統中有大量非阻塞調用,能隨時產生事件的時候,selector機制才能發揮最大的威力。

部分程式設計語言中,對非同步編程的支援就止步於此(不含語言官方之外的擴充)。需要程式猿直接使用epoll去註冊事件和回調、維護一個事件迴圈,然後大多數時間都花在設計回呼函數上。
不論什麼程式設計語言,但凡要做非同步編程,上述的“事件迴圈+回調”這種模式是逃不掉的,儘管它可能用的不是epoll,也可能不是while迴圈。但是使用的非同步方式基本都是 “等會兒告訴你” 模型的非同步方式。

但是在asyncio非同步編程中為什麼沒有看到 CallBack 模式呢?因為 Python 非同步編程是用了協程幫我們取代了回調

python 並發編程

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.