進程 python
1、基本概念程序員
進程是系統資源分配的最小單位, 程序隔離的邊界系統由一個個進程(程序)組成。通常狀況下,包括文本區域(text region)、數據區域(data region)和堆棧(stack region)算法
所以進程的建立和銷燬都是相對於系統資源,因此是一種比較昂貴的操做。 進程有三個狀態:數據庫
進程是爲了提升CPU的執行效率,減小由於程序等待帶來的CPU空轉以及其餘計算機軟硬件資源的浪費而提出來的,也讓併發成爲可能。現假設一個進程內有A、B兩個I/O任務而且CPU爲雙核,若是沒有多進程的實現,CPU在執行時只能利用到一個核的資源: 先等待A任務的讀取-寫入操做所有執行完畢後,而後才能去執行B任務。但這對於CPU來講是無疑是極大的浪費,既須要等待A任務的耗時操做,又不能利用到CPU多核資源。因此多進程是讓CPU執行某個耗時任務時(網絡延遲、睡眠等待等),建立一個新的子進程,讓子進程去執行任務B,等待任務A的耗時操做完畢以後,CPU再切換到另外一個任務執行。注意,由於涉及到切換那麼就須要有一個東西來記錄當前這個進程的狀態,好比進程運行時所需的系統資源:內存,硬盤,鍵盤等(地址空間,全局變量,文件描述符,各類硬件等等),因此進程的意義就在於經過進程來記錄任務所擁有的系統資源&標識任務。進程狀態的記錄、恢復、切換稱之爲上下文切換,但因爲CPU執行速度飛快,所以使得看上去就像是多個進程在同時進行.緩存
進程是系統進行資源分配和調度的一個獨立單位,擁有本身獨立的堆和棧,但既不共享堆,亦不共享棧,每一個進程都是獨立的,各自持有一份數據沒法共享,因此爲了可以讓進程之間實現數據共享:安全
1.Queue是多進程中安全的隊列,可使用Queue實現多進程之間的數據傳遞。服務器
其中put方法用於插入數據到隊列中,有兩個可選參數:blocked和timeout。若是blocked爲True(默認值),而且timeout爲正值,該方法會阻塞timeout指定的時間,直到該隊列有剩餘的空間。若是超時,會拋出Queue.Full異常。若是blocked爲False,但該Queue已滿,會當即拋出Queue.Full異常網絡
get方法能夠從隊列讀取而且刪除一個元素。一樣,get方法有兩個可選參數:blocked和timeout。若是blocked爲True(默認值),而且timeout爲正值,那麼在等待時間內沒有取到任何元素,會拋出Queue.Empty異常。若是blocked爲False,有兩種狀況存在,若是Queue有一個值可用,則當即返回該值,不然,若是隊列爲空,則當即拋出Queue.Empty異常多線程
2.pp = multiprocessing.Pipe() Pipe方法返回(conn1, conn2)表明一個管道的兩個端。併發
Pipe方法有duplex參數,若是duplex參數爲True(默認值),那麼這個管道是全雙工模式,也就是說conn1和conn2都可收發。duplex爲False,conn1只負責接受消息,conn2只負責發送消息
管道的兩個端還有send方法負責發送消息,和recv方法負責接受消息。例如,在全雙工模式下,能夠調用conn1.send發送消息,conn1.recv接收消息。若是沒有消息可接收,recv方法會一直阻塞。若是管道已經被關閉,那麼recv方法會拋出EOFError
3、進程池
目的:重複利用進程池中原先創建好的的進程,簡化進程的建立、銷燬、任務分配.使用完後的進程由進程池管理
地球語言:進程池是事先劃分一塊系統資源區域,這組資源區域在服務器啓動時就已經建立和初始化,用戶若是想建立新的進程,能夠直接取得資源,從而避免了動態分配資源(這是很耗時的)。
進程池中的全部子進程都運行着相同的代碼,並具備相同的屬性,好比優先級、 PGID 等。當有新的任務來到時,主進程將經過某種方式選擇進程池中的某一個子進程來爲之服務。相比於動態建立子進程,選擇一個已經存在的子進程的代價顯得小得多。進程池內子進程的個數可經過maxsize參數指定。
主進程選擇子進程的兩種方式:
當選擇好子進程後,須要在主進程和子進程之間預先創建好一條管道,而後經過管道或者Queue隊列來實現全部的進程間通訊。注 : 進程池建立的進程 默認都是守護進程,只要主進程結束,子進程就結束。
提交進程池的方式有同步提交&異步提交,同步提交的方式必須是一個子進程執行完纔會執行下一個,因此主進程是最後結束的,會等待進程池中全部進程結束; 可是異步方式(子進程同時執行)必須結合close&join使用,pool.close() 必須先關閉進程池,表示不容許再添加新的任務,再使用pool.join()表示等待進程池中全部進程結束,主進程再繼續執行。
線程
1、基本概念
線程是程序執行最小單元。線程擁有本身獨立的棧和共享的堆,共享堆,但不共享棧。一個進程內能夠有多個線程(至少一個主線程),多個線程共享該進程的全部變量。
線程是爲了減小進程切換和建立的開銷,提升執行效率和節省資源,提升系統的併發性,解決一個進程只能執行一個任務的缺陷,使得進程內併發成爲可能。假設,一個文本程序,須要接受鍵盤輸入,將內容顯示在屏幕上,還須要保存信息到硬盤中。但只有一個進程執行此任務,那麼勢必形成同一時間只能執行其中一個任務的局面(當保存時,就不能經過鍵盤輸入內容)。若是使用多進程來實現的話:每一個進程只負責其中一個任務,例如,進程A負責接收鍵盤輸入的任務,進程B負責將內容顯示在屏幕上的任務,進程C負責保存內容到硬盤中的任務。但此時進程A,B,C間的協做涉及到了進程通訊問題,並且都有共同須要的東西 --- 文本內容,但進程之間本來是不支持數據共享的,即便能夠經過隊列實現數據共享,但不停的切換會形成性能上的損失,並且由於一個簡單的任務而要額外的建立子進程,還得給它分配主進程全部的資源(諸如系統資源,地址空間,全局變量等),對於CPU來講過小題大作了。如有一種機制,可使任務A,B,C共享資源,這樣上下文切換所須要保存和恢復的內容就少了,同時又能夠減小通訊所帶來的性能損耗,因此線程就是這種機制,線程共享進程的大部分資源,並參與CPU的調度, 固然線程本身也是擁有本身的資源的,例如,棧,寄存器等等。 此時進程同時也是線程的容器。但線程也有着本身的缺陷的,若一個線程掛掉了,一整個進程也掛掉了,這意味着其它線程也掛掉了,進程卻沒有這個問題,一個進程掛掉,另外的進程仍是活着,由於進程之間都是擁有獨立的資源,互不干擾的。
進程切換:
線程切換僅需第二、3步,所以進程的切換代價比較大,但相比進程不夠穩定容易丟失數據。
class Mythread(threading.Thread):
2.自定義線程類中重寫 run方法,再根據業務需求在run方法中添加邏輯代碼。
def run(self):
3.經過 自定義線程類的實例化對象調用start() 方法,啓動自定義線程
mythread.start()
注:使用實例化對象調用start方法,start會自動調用 run方法,若在建立實例對象須要進行傳參,要重寫 __init__ 方法,可是必須初始化父類的 __init__ 方法,否則沒法調用自定義線程類中重寫的 run方法
方法一:
def __init__(self, num):
super().__init__()
self.num = num
方法二:
def __init__(self, num):
super(當前類名, self).__init__()
self.num = num
解決機制:1.同步解決,按照順序依次執行每一個線程。先等待線程A執行完畢,再執行線程B。缺陷:本來能使用多個線程實現多任務操做,如此的話,多線程就沒有存在的意義了,程序又變成了單任務。
2. 異步解決,互斥鎖用於防止兩條線程同時對同一公共資源(好比全局變量)進行讀寫的機制,經過將代碼切片成一個一個的臨界區域而達成的。
地球語言:當線程A對某個全局變量進行操做以前,將其上鎖,此時就算CPU切換到線程B它也沒法解鎖沒法操做,只有當線程A真正操做完畢解鎖以後,線程B纔可以執行它的操做
互斥鎖底層:
互斥鎖實質是一種變量,上鎖解鎖實際上是給它置0、置1的操做。現假設mutex=1時表示鎖是空閒的,若是此時某個線程調用acquire( )函數【上鎖】就能夠得到鎖資源,而且mutex此時被置爲0。
當mutex=0時表示鎖正在被其餘線程佔用,若是此時有其餘線程也調用acquire( )函數來得到鎖時會被掛起等待。
流程:
mutex = threading.Lock # 建立互斥鎖,默認鎖是開啓狀態
mutex.acquire( ) # 上鎖
mutex.release( ) # 解鎖
互斥鎖可能致使的死鎖問題:
死鎖:線程間共享多個資源的時候,若是兩個線程分別佔有⼀部分資源而且同時等待對⽅的資源,而處於的一種永久等待狀態,就會形成死鎖。
地球語言:假設A線程持有鎖a,B線程持有鎖b,而主線程訪問臨界區的條件是須要同時具備鎖a和鎖b,那麼A就會等待B釋放鎖b,B會等待A釋放鎖a,若是沒有一種措施,他兩會一直等待,這樣就產生了死鎖。
如何產生的死鎖:
一、系統資源不足:若是系統資源足夠,每一個申請鎖的線程都能後得到鎖,那麼產生死鎖的狀況就會大大下降;
二、申請鎖的順序不當:當兩個線程按照不一樣的順序申請、釋放鎖資源時也會產生死鎖。
死鎖產生的條件:
一、互斥屬性:即每次只能有一個線程佔用資源。
二、請求與保持:即已經申請到鎖資源的線程能夠繼續申請。在這種狀況下,一個線程也能夠產生死鎖狀況,即抱着鎖找鎖。
三、不可剝奪:線程已經獲得鎖資源,在沒有本身主動釋放以前,不能被強行剝奪。
四、循環等待:多個線程造成環路等待,每一個線程都在等待相鄰線程的鎖資源。
死鎖的避免:
一、既然死鎖的產生是因爲使用了鎖,那麼若是能夠在不使用鎖的狀況下完成任務,就儘可能不使用互斥鎖機制。若是有多種方案都能實現,那麼儘可能不選用帶鎖的這種方案
二、儘可能避免同時得到多把鎖,若是有必要,就要保證得到鎖的順序相同。
三、只有一把互斥鎖存在時,尤爲注意互斥鎖使用完畢且代碼即將終止前,必須解鎖。
四、儘量少的鎖定所競爭的資源代碼
五、將鎖加入到隊列中,由於隊列遵循的是先進先出規範。
4、GIL鎖
GIL鎖:Global Interpreter Lock 全局解釋器鎖,互斥鎖保證的是線程間公共數據資源的安全。GIL也是一種特殊的互斥鎖,解釋器被這個GIL全局解釋器鎖保護着,它確保任什麼時候候都只有一個Python線程執行。同一進程內在同一時間只能有一個線程使用到CPU(僞併發)。GIL並非Python的特性,GIL只在CPython解釋器上存在,Python徹底能夠不依賴於GIL。
基本概念:任何Python線程執行前,必須先得到GIL鎖,而後每執行100條字節碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執行。但在此線程執行期間別的線程沒法使用CPU,只有等待當前上鎖的線程執行完畢解鎖後纔會切換到下一個線程。這個GIL全局鎖實際上把全部線程的執行代碼都給上了鎖,因此多線程在Python中只能交替執行,即便100個線程跑在100核CPU上,也只能用到1個核。
互斥鎖&GIL鎖:
互斥鎖保證的是讓當前的線程把事情作完再解鎖給下一個線程 , GIL鎖保證的是同一時間只能有一個線程在執行,但GIL不能保證這個線程所有執行完了再讓CPU去調度下一個,可能線程1只執行了20%就被cpu切換到線程2 , 因此就算有GIL鎖若是存在公共資源競爭的狀況也必須同時存在互斥鎖 . 互斥鎖:邏輯上只有一個線程執行 , GIL:調度上只有一個線程執行 。
Python語⾔和GIL沒有半⽑錢關係。僅僅是因爲歷史緣由在Cpython虛擬機(解釋器),難以移除GIL。
GIL鎖釋放時機:
GIL鎖解決方案:
協程
1、基本概念
能夠暫停執行的函數,協程經過在線程中實現調度,避免了陷入內核級別的上下文切換形成的性能損失,進而突破了線程在IO上的性能瓶頸。 當涉及到大規模的併發鏈接時,例如10K鏈接。以線程做爲處理單元,系統調度的開銷仍是過大。在實現多任務時, 線程切換從系統層面遠不止保存和恢復 CPU上下文這麼簡單。 操做系統爲了程序運行的高效性,每一個線程都有本身緩存Cache等等數據,操做系統還會幫你作這些數據的恢復操做。因此線程的切換很是耗性能。可是協程的切換隻是單純的操做CPU的上下文,因此⼀秒鐘切換個上百萬次系統都抗的住。
當鏈接數不少 —> 須要大量的線程來幹活 —> 可能大部分的線程處於ready狀態 —> 系統會不斷地進行上下文切換。既然性能瓶頸在上下文切換,那乾脆就在線程中本身實現調度,不陷入內核級別的上下文切換,協程切換隻涉及到CPU的上下文,因此在線程內實現協程可大大提高性能。
- 協程屬於線程。協程運行在線程裏面,所以協程又稱微線程和纖程等
- 協程沒有線程的上下文切換消耗。協程的調度切換是用戶(程序員)手動切換的,所以更加靈活,所以又叫用戶空間線程.
- 原子操做性。因爲協程是用戶調度的,因此不會出現執行一半的代碼片斷被強制中斷了,所以無需原子操做鎖。
通俗的理解:在⼀個線程中的某個函數,能夠在任何地方保存當前函數的⼀些臨時變量等信息,而後切換到另外⼀個函數中執⾏,注意不是經過調用函數的方式作到的,而且切換的次數以及何時再切換到原來的函數都由開發者本身肯定。
Python中,使用了yield的函數爲生成器,生成器一次只返回一個數據,而且能夠屢次返回值。中途能夠暫停一下,轉而執行其餘代碼,再回來繼續執行函數往下的代碼。
可迭代對象:能夠經過 for ... in... 的循環語法,迭代讀取數據的叫作可迭代對象,可以使用 isinstance(對象,Iterable) 判斷一個對象是不是可迭代對象
可迭代對象包括 : Python中的字符串,元組,字典,列表,集合,range 。 不可迭代類型 : 數字類型,整型,浮點型,自定義的普通類。
迭代器:實現可迭代對象進行遍歷的工具,幫助可迭代對象記錄當前的迭代位置,而且返回下一個數據的結果。迭代器規定了對象能夠經過next方法返回下一個值。
可迭代對象&迭代器 : 可迭代對象包含迭代器。一個對象所屬的類中實現了__iter__方法(調用一次),那麼它就是一個可迭代對象;若是一個對象所屬的類中實現了
__next__方法(調用n次),那麼它是一個迭代器。可以使用 isinstance(對象,Iterator) 判斷一個對象是不是迭代器。自定義可迭代對象時, 必須實現 __iter__ 方法,;自定義迭代器時,必須實現__iter__和__next__方法。當調用iter(迭代器對象)時會調用__iter__()方法,當調用next(迭代器對象)時會調用__next__()方法。若是自定義迭代器類時,同時實現了__iter__和__next__方法,那麼__iter__方法返回自身便可(self)。
__iter__方法返回的是:當前對象的迭代器類的實例對象 ; __next__方法返回迭代的每一步,當迭代完最後一個數據後,再次調用next拋出StopIteration異常。
class MyList(object): def __init__(self): self.li = list() def add_item(self, elem): self.li.append(elem) def __iter__(self): return MyIterator(self.li) class MyIterator(object): def __init__(self, li): self.li = li self.count = 0 def __iter__(self): pass def __next__(self): if self.count < len(self.li): self.count += 1 return self.li[self.count] else: return StopIteration # TypeError: iter() returned non-iterator of type 'MyIterator'
生成器: 是⼀類特殊的迭代器,已經自動實現了__iter__和__next__方法,無需再手動實現。生成器在迭代過程當中能夠修改當前迭代值,但修改普通迭代器的當前迭代值每每會發生異常。Python中有兩種建立生成器的方式
- 使用 yield 關鍵字的函數,能夠屢次返回後面的值給調用者,生成器實際上也算是實現了迭代器接口(協議)。即生成器也可經過next返回下一個值。
- 把列表推導式的中括號換成小括號(存儲的是生成列表的方式,不佔空間,再經過for循環就可取出)
yield與return不一樣的是:return以後函數會被釋放,而生成器不會。
yield關鍵字:
- yield 關鍵字會返回跟在後面的結果
- 暫停程序,保存程序狀態,將函數掛起,直到下次調用next/send,再次回到上次暫停的位置
- 調用next()/send(),可讓生成器從上一次的斷點處繼續執行(喚醒生成器),並獲得生成器中yield返回的結果。
- yield關鍵字須要一個變量來接收它後面的結果
注:第一次啓動生成器時,最好使用next方法,若執意使用send,給定參數爲None。生成器中使用return會終止生成器,並拋出異常。
生成器做用:例如,當生成一個包含大量元素的集合時,並非像列表同樣一次性所有返回,而是根據需求使用next方法取值,可大量節約內存空間。
3、協程的實現
只使用一個線程(單線程),在一個線程中規定某個函數的執行順序:一個函數執行後切換到另外一個函數執行,切換不是經過調用函數作到的,切換的次數和什麼時候切換都由開發者自定義。可理解爲單線程上的多任務/生成器
協程 - yield : 建立兩個生成器 , 在主程序中使用死循環切換執行兩個任務
def work1() print(「w1」) yield def work2 print(「w2」) yield w1 = worl1() w2 = work2() while True: next(w1) next(w2)
協程 - greenlent:
sudo pip3 install greenlet # 0.安裝模塊 from greenlet import greenlet # 1. 導入模塊 gre1 = greenlet(work1) # 2. 建立協程 gre1.switch(*args, **kwargs) # 3. 啓動協程
協程 - gevent
sudo pip3 install gevent # 0.安裝模塊 import gevent # 1. 導入模塊 g1 = gevent.spawn(work1, args, kwds) # 2. 建立並運行 # 3. gevent會根據耗時操做自動切換 # 例如 : gevent.sleep() # 4. 等待協程執行結束 g1.join() # 須要打補丁:gevent默認不識別其他耗時操做 : read,recv,accept,sleep... # 5. 導入模塊 from gevent import monkey # 6. 破解 monkey.patch_all() # 7.獲取當前協程的信息: gevent.getcurrent() --> 返回一個greenlet對象
打補丁做用:無需再手動切換協程,會根據阻塞自動切換。
三者比較
應用場景:
進程:CPU密集型(使用多核)
線程:io密集型(網絡io,文件io,數據庫io)
協程:io密集型(網絡io),非阻塞異步併發
多核CPU,CPU密集型應用:多進程
多核CPU,IO密集型應用:多線程 // 多協程
單核CPU,CPU密集型應用:單進程
單核CPU,IO密集型應用:多協程
性能比較:
消耗大小:進程>線程>協程
切換速度:協程>線程>進程
三者區別:
進程:資源分配的最小單位,是線程的容器。進程間數據不共享 ,每一個進程各自有獨立運行空間,殺死其中一個進程不會影響其餘的進程 , 穩定安全,建立銷燬切換耗時長。
一個程序至少一個進程,一個進程至少一個線程 ; 線程不能獨立運行,必須依存於進程中。需充分使用多核性能的使用進程。 multiprocessing.Process
線程:CPU真正調度的單位,必須依賴於進程,線程之間共享進程的運行空間,共享數據資源,殺死其中一個線程死 ,可能會影響到別的線程 ,建立銷燬切換耗時更短,穩定和安全差些(死鎖)
互斥鎖保證的是讓當前的線程把事情作完再解鎖給下一個線程 , GIL鎖保證的是同一時間只能有一個線程在執行,但GIL不能保證這個線程所有執行完了再讓cpu去調度下一個,可能線程1只執行了20%,就被cpu切換到線程2 ,因此就算有GIL鎖也必須同時存在互斥鎖 . 互斥鎖:邏輯上只有一個線程執行 , GIL:調度上只有一個線程執行。 threading.Thread
協程: 用戶級,輕量級線程。用戶控制,單線程中的多任務,建立銷燬切換耗時最短。 yield、next、greenlet、switch、gevent、spwan
一、進程多與線程進行比較
二、協程與線程進行比較
並行 : 任務數 <= 核數 ; 真正的 多個任務同時執行
併發 : 任務數 > 核數 ; 快速的依次執行多個任務
指的是任務數小於等於CPU核數,即同一時刻多個任務真的是⼀起執行的,真正的並行執行多任務只能在多核CPU上實現,可是因爲任務數量遠遠多於CPU的核心數量,因此操做系統也會自動把不少任務輪流調度到每一個核心上執行。單線程永遠沒法達到並行狀態,另外的,因爲Python的多線程GIL鎖的存在,對於Python來講沒法經過多線程到達並行狀態。
指的是任務數多於cpu核數,經過操做系統的各類任務調度算法,實現用多個任務「⼀起」執行(實際上總有⼀些任務不在執行,由於切換任務的速度至關快,看上去的假象是一塊兒執行)
併發設計的標準:使多個操做能夠在重疊的時間段內進行,重疊時間能夠理解爲一段時間內。例如:在時間1s秒內, 具備IO操做的task1和task2都完成,就能夠說是併發執行。因此單線程也是能夠作到併發運行的。固然並行確定是併發的。一個程序可否併發執行,取決於設計,也取決於部署方式。例如, 當給程序開一個線程(協程是不開的)它不多是併發的,由於在重疊時間內根本就沒有兩個task在運行。當一個程序被設計成完成一個任務再去完成下一個任務的時候,即使部署是多線程多協程的也是沒法達到併發運行的。
並行與併發的關係: 併發的設計使到併發執行成爲可能,而並行是併發執行的其中一種模式。