python之併發編程(概念篇)

一.進程  

1.什麼是進程

進程是正在進行的一個過程或者一個任務。而負責執行任務的則是cpu。python

2.進程與程序的區別

程序並不能單獨運行,只有將程序裝載到內存中,系統爲它分配資源才能運行,而這種執行的程序就稱之爲進程。程序和進程的區別就在於:程序是指令的集合,它是進程運行的靜態描述文本;進程是程序的一次執行活動,屬於動態概念。同一個程序執行兩次是兩個進程linux

在多道編程中,咱們容許多個程序同時加載到內存中,在操做系統的調度下,能夠實現併發地執行。這是這樣的設計,大大提升了CPU的利用率。進程的出現讓每一個用戶感受到本身獨享CPU,所以,進程就是爲了在CPU上實現多道編程而提出的。程序員

3.併發與並行

不管是並行仍是併發,在用戶看來都是'同時'運行的,而一個cpu同一時刻只能執行一個任務。web

1)併發:是僞並行,即某個時段看起來是同時運行。單個cpu+多道技術就能夠實現併發,(並行也屬於併發)數據庫

2)並行:同時運行,只有具有多個cpu才能實現並行。編程

單核下,能夠利用多道技術,多核中的每一個核也均可以利用多道技術(多道技術是針對單核而言的緩存

有四個核,六個任務,這樣同一時間有四個任務被執行,假設分別被分配給了cpu1,cpu2,cpu3,cpu4,tomcat

一旦任務1遇到I/O就被迫中斷執行,此時任務5就拿到cpu1的時間片去執行,這就是單核下的多道技術安全

而一旦任務1的I/O結束了,操做系統會從新調用它(需知進程的調度、分配給哪一個cpu運行,由操做系統說了算),可能被分配給四個cpu中的任意一個去執行。服務器

4.同步\異步and阻塞\非阻塞

同步:A調用B,B處理直到得到結果,才返回給A。

須要調用者一直等待和確認調用結果是否返回, 而後繼續往下執行。 

異步:A調用B,B直接返回。無需等待結果,B經過狀態,通知等來通知A或回調函數來處理。
調用結果返回時, 會以消息或回調的方式通知調用者。

阻塞:A調用B,A被掛起直到B返回結果給A,A繼續執行。
調用結果返回前,當前進程掛起不可以處理其餘任務,一直等待調用結果返回。

非阻塞:A調用B,A不會被掛起,A能夠執行其餘操做。

調用結果返回前,當前進程不掛起, 能夠去處理其餘任務。

因此,同步異步說的是被調用者結果返回時通知進程的一種通知機制,阻塞非阻塞說的是調用結果返回前進程的狀態,是掛起仍是繼續處理其餘任務。

 

二.線程

1.什麼是線程

線程是操做系統可以進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運做單位。一條線程指的是進程中一個單一順序的控制流,一個進程中能夠併發多個線程,每條線程並行執行不一樣的任務。

2.線程與進程的區別

1)線程共享建立它的進程的內存空間,進程的內存是獨立的

2)線程能夠直接訪問其進程的數據段,進程擁有父進程的數據段副本

3)線程能夠直接與其進程的其餘線程通訊,進程間通訊須要經過一箇中間代理來實現。

4)新線程很容易建立,建立新進程須要對父進程實現一次複製。
5)線程能夠控制和操做同一進程裏的其餘線程,進程只能控制子進程。
6)對主線程的更改(取消,優先級更改等)可能會影響到進程中其餘線程的行爲; 對父進程的更改不會影響子進程。

3.多線程

多線程指的是,在一個進程中開啓多個線程,簡單的講:若是多個任務共用一塊地址空間,那麼必須在一個進程內開啓多個線程。

詳細的講分爲4點:

 

1. 多線程共享一個進程的地址空間。

2. 線程比進程更輕量級,線程比進程更容易建立可撤銷,在許多操做系統中,建立一個線程比建立一個進程要快10-100倍,在有大量線程須要動態和快速修改時,這一特性頗有用

3. 若多個線程都是cpu密集型的,那麼並不能得到性能上的加強,可是若是存在大量的計算和大量的I/O處理,擁有多個線程容許這些活動彼此重疊運行,從而會加快程序執行的速度。

4. 在多cpu系統中,爲了最大限度的利用多核,能夠開啓多個線程,比開進程開銷要小的多。(這一條並不適用於python)

 

三.GIL全局解釋器鎖

 

每一個線程在執行的過程當中都須要先獲取GIL,保證同一時刻只有一個線程在運行,目的是解決多線程同時競爭程序中的全局變量而出現的線程安全問題。它並非python語言的特性,僅僅是因爲歷史的緣由在CPython解釋器中難以移除,由於python語言運行環境大部分默認在CPython解釋器中。

因爲之前的電腦基本都是單核CPU,多線程和單線程幾乎看不出差異,但是因爲計算機的迅速發展,如今的電腦幾乎都是多核CPU了,最少也是兩個核心數的,這時差異就出來了:經過以前的案例咱們已經知道,即便在多核CPU中,多線程同一時刻也只有一個線程在運行,這樣不只不能利用多核CPU的優點,反而因爲每一個線程在多個CPU上是交替執行的,致使在不一樣CPU上切換時形成資源的浪費,反而會更慢。即緣由是一個進程只存在一把gil鎖,當在執行多個線程時,內部會爭搶gil鎖,這會形成當某一個線程沒有搶到鎖的時候會讓cpu等待,進而不能合理利用多核cpu資源。

四.協程

1.協程是什麼?

協程是單線程下的併發,一種用戶態的輕量級線程,即協程是由用戶程序本身控制調度的。

2.注意:

1) python的線程屬於內核級別的,即由操做系統控制調度(如單線程遇到io或執行時間過長就會被迫交出cpu執行權限,切換其餘線程運行)

2) 單線程內開啓協程,一旦遇到io,就會從應用程序級別(而非操做系統)控制切換,以此來提高效率(!!!非io操做的切換與效率無關)

3)操做系統控制線程的切換,用戶在單線程內控制協程的切換

3.優勢與缺點:

優勢:

協程的切換開銷更小,屬於程序級別的切換,操做系統徹底感知不到,於是更加輕量級,單線程內就能夠實現併發的效果,最大限度地利用cpu。

缺點:

協程的本質是單線程下,沒法利用多核,(能夠是一個程序開啓多個進程,每一個進程內開啓多個線程,每一個線程內開啓協程)

協程指的是單個線程,於是一旦協程出現阻塞,將會阻塞整個線程

4.總結

1)必須在只有一個單線程裏實現併發

2)修改共享數據不需加鎖

3)用戶程序裏本身保存多個控制流的上下文棧

4)一個協程遇到IO操做自動切換到其它協程(如何實現檢測IO,yield、greenlet都沒法實現,就用到了gevent模塊(select機制)

 

五.IO模型

1.IO模型介紹

IO發生時涉及的對象和步驟。對於一個network IO (這裏咱們以read舉例),它會涉及到兩個系統對象,一個是調用這個IO的process (or thread),另外一個就是系統內核(kernel)。當一個read操做發生時,該操做會經歷兩個階段:

1)等待數據準備

2)將數據從內核拷貝到進程中

不一樣IO模型的區別就是在兩個階段上各有不一樣的狀況

補充

#一、輸入操做:read、readv、recv、recvfrom、recvmsg共5個函數,若是會阻塞狀態,則會經歷wait data和copy data兩個階段,若是設置爲非阻塞則在wait 不到data時拋出異常

#二、輸出操做:write、writev、send、sendto、sendmsg共5個函數,在發送緩衝區滿了會阻塞在原地,若是設置爲非阻塞,則會拋出異常

#三、接收外來連接:accept,與輸入操做相似

#四、發起外出連接:connect,與輸出操做相似

 

2.阻塞IO

默認狀況下全部的socket都是阻塞的

 

 

 

 

 當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備數據。對於network io來講,不少時候數據在一開始尚未到達(好比,尚未收到一個完整的UDP包),這個時候kernel就要等待足夠的數據到來。

而在用戶進程這邊,整個進程會被阻塞。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,而後kernel返回結果,用戶進程才解除block的狀態,從新運行起來。因此,blocking IO的特色就是在IO執行的兩個階段(等待數據和拷貝數據兩個階段)都被block了

除非特別指定,幾乎全部的IO接口 ( 包括socket接口 ) 都是阻塞型的。這給網絡編程帶來了一個很大的問題,如在調用recv(1024)的同時,線程將被阻塞,在此期間,線程將沒法執行任何運算或響應任何的網絡請求。

  一個簡單的解決方案:

#在服務器端使用多線程(或多進程)。多線程(或多進程)的目的是讓每一個鏈接都擁有獨立的線程(或進程),這樣任何一個鏈接的阻塞都不會影響其餘的鏈接。

    該方案的問題是:

#開啓多進程或都線程的方式,在遇到要同時響應成百上千路的鏈接請求,則不管多線程仍是多進程都會嚴重佔據系統資源,下降系統對外界響應效率,並且線程與進程自己也更容易進入假死狀態。

    改進方案:    

#不少程序員可能會考慮使用「線程池」或「鏈接池」。「線程池」旨在減小建立和銷燬線程的頻率,其維持必定合理數量的線程,並讓空閒的線程從新承擔新的執行任務。
「鏈接池」維持鏈接的緩存池,儘可能重用已有的鏈接、減小建立和關閉鏈接的頻率。
這兩種技術均可以很好的下降系統開銷,都被普遍應用不少大型系統,如websphere、tomcat和各類數據庫等。

    改進後方案其實也存在着問題:

#「線程池」和「鏈接池」技術也只是在必定程度上緩解了頻繁調用IO接口帶來的資源佔用。
並且,所謂「池」始終有其上限,當請求大大超過上限時,「池」構成的系統對外界的響應並不比沒有池的時候效果好多少。
因此使用「池」必須考慮其面臨的響應規模,並根據響應規模調整「池」的大小。

對應上例中的所面臨的可能同時出現的上千甚至上萬次的客戶端請求,「線程池」或「鏈接池」或許能夠緩解部分壓力,可是不能解決全部問題。總之,

多線程模型能夠方便高效的解決小規模的服務請求,但面對大規模的服務請求,多線程模型也會遇到瓶頸,能夠用非阻塞接口來嘗試解決這個問題。

3.非阻塞IO

能夠經過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操做時,流程是這個樣子:

 

 

當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講 ,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而用戶就能夠在本次到下次再發起read詢問的時間間隔內作其餘事情,或者直接再次發送read操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它立刻就將數據拷貝到了用戶內存(這一階段仍然是阻塞的),而後返回

也就是說非阻塞的recvform系統調用調用以後,進程並無被阻塞,內核立刻返回給進程,若是數據還沒準備好,此時會返回一個error。進程在返回以後,能夠乾點別的事情,而後再發起recvform系統調用。重複上面的過程,循環往復的進行recvform系統調用。這個過程一般被稱之爲輪詢。輪詢檢查內核數據,直到數據準備好,再拷貝數據到進程,進行數據處理。須要注意,拷貝數據整個過程,進程仍然是屬於阻塞的狀態。

能夠看到用戶進程須要一直主動詢問kernel數據準備好了沒有,帶來的問題就是

循環調用recv()將大幅度推高CPU佔用率,在低配主機下極容易出現卡機狀況。

任務完成的響應延遲增大了,由於每過一段時間纔去輪詢一次read操做,而任務可能在兩次輪詢之間的任意時間完成。這會致使總體數據吞吐量的下降。

 此外,在這個方案中recv()更多的是起到檢測「操做是否完成」的做用,實際操做系統提供了更爲高效的檢測「操做是否完成「做用的接口,例如select()多路複用模式,能夠一次檢測多個鏈接是否活躍。

4.多路複用IO

有些地方也稱這種IO方式爲事件驅動IO。咱們都知道,select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:

 

 

 

當用戶進程調用了select,那麼整個進程會被block,而同時,kernel會「監視」全部select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操做,將數據從kernel拷貝到用戶進程。
 這個圖和blocking IO的圖其實並無太大的不一樣,事實上還更差一些。由於這裏須要使用兩個系統調用(select和recvfrom),而blocking IO只調用了一個系統調用(recvfrom)。可是,用select的優點在於它能夠同時處理多個connection。

強調:

1)若是處理的鏈接數不是很高的話,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優點並非對於單個鏈接能處理得更快,而是在於能處理更多的鏈接。

2)在多路複用模型中,對於每個socket,通常都設置成爲non-blocking,可是,如上圖所示,整個用戶的process實際上是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。

 結論: select的優點在於能夠處理多個鏈接,不適用於單個鏈接  

 

select監聽fd變化的過程分析:

#用戶進程建立socket對象,拷貝監聽的fd到內核空間,每個fd會對應一張系統文件表,內核空間的fd響應到數據後,就會發送信號給用戶進程數據已到;
#用戶進程再發送系統調用,好比(accept)將內核空間的數據copy到用戶空間,同時做爲接受數據端內核空間的數據清除,這樣從新監聽時fd再有新的數據又能夠響應到了(發送端由於基於TCP協議因此須要收到應答後纔會清除)。

    該模型的優勢:

#相比其餘模型,使用select() 的事件驅動模型只用單線程(進程)執行,佔用資源少,不消耗太多 CPU,同時可以爲多客戶端提供服務。若是試圖創建一個簡單的事件驅動的服務器程序,這個模型有必定的參考價值。

    該模型的缺點:

#首先select()接口並非實現「事件驅動」的最好選擇。由於當須要探測的句柄值較大時,select()接口自己須要消耗大量時間去輪詢各個句柄。不少操做系統提供了更爲高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。若是須要實現更高效的服務器程序,相似epoll這樣的接口更被推薦。遺憾的是不一樣的操做系統特供的epoll接口有很大差別,因此使用相似於epoll的接口實現具備較好跨平臺能力的服務器會比較困難。
#其次,該模型將事件探測和事件響應夾雜在一塊兒,一旦事件響應的執行體龐大,則對整個模型是災難性的。

5.異步IO

 

 用戶進程發起read操做以後,馬上就能夠開始去作其它的事。而另外一方面,從kernel的角度,當它受到一個asynchronous read以後,首先它會馬上返回,因此不會對用戶進程產生任何block。而後,kernel會等待數據準備完成,而後將數據拷貝到用戶內存,當這一切都完成以後,kernel會給用戶進程發送一個signal,告訴它read操做完成了。

而後因爲不穩定等緣由異步IO用的並很少。

相關文章
相關標籤/搜索