Python:線程、進程與協程(1)——概念

        最近的業餘時間主要放在了學習Python線程、進程和協程裏,第一次用python的多線程和多進程是在兩個月前,當時只是簡單的看了幾篇博文然後就跟着用,沒有仔細去研究,第一次用的感覺它們其實挺簡單的,最近這段時間通過看書, 看Python 中文官方文檔等等相關資料,發現並沒有想想中的那麼簡單,很多知識點需要仔細去理解,Python線程、進程和協程應該是Python的高級用法。Python的高級用法有很多,看看Python 中文官方文檔就知道了,當然有時間看看這些模塊是怎麼實現的對自己的提高是很有幫助的。選擇了編程這個行業,就是要不斷的學習、思考、歸納總結經驗,路漫漫其修遠兮,吾將上下而求索,希望能與各位共勉。接下來要花好幾篇博文的篇幅來講講我學習線程、進程和協程的經驗,有講得不好的地方,希望大家批評指正。這篇博文主要講講與之有關的概念。

(一)線程與多線程

        線程

       (1) 線程,有時被稱爲輕量級進程(Lightweight Process,LWP),是程序執行流的最小單元。

       (2)一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。有了這些它能夠記錄自己運行到了什麼地方,可以稱爲線程的上下文。

      (3)線程的運行可能被搶佔(中斷)或暫時的被掛起(也叫睡眠)讓其它的線程運行,這叫做讓步。

      (4)線程也有就緒、阻塞和運行三種基本狀態。就緒狀態是指線程具備運行的所有條件,邏輯上可以運行,在等待處理機;運行狀態是指線程佔有處理機正在運行;阻塞狀態是指線程在等待一個事件(如某個信號量),邏輯上不可執行。

     (5)線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不獨立擁有系統資源,但它可與同屬一個進程的其它線程共享該進程所擁有的全部資源。

     多線程

     (1)每一個應用程序都至少有一個進程和一個線程。線程是程序中一個單一的順序控制流程。在單個程序中同時運行多個線程完成不同的被劃分成一塊一塊的工作,稱爲多線程。

 很好理解,開發軟件就是每個人或者每個小組負責一個模塊,當所有人(組)相關的模塊都編寫完後,就開始合併代碼,然後就測試,修復bug。

   (2)除非代碼依賴第方資源,否則在單處理器的機器上使用多線程,不會加速代碼的執行速度,甚至會增加一些線程管理的開銷。

實際上,在單處理器的系統中,每個線程會被安排成每次只運行一小會,然後就把CPU讓出來,讓其它的線程去運行。比如線程切換等等,這些都是要花費資源和時間的,所以在單CPU機器上使用多線程有時候不但感覺不到執行速度變快,反而變慢了。

   (3)多線程會從多處理器或者多核的機器上獲益,它會在每個處理器上並行執行每個線程,從而提高執行速度。

  (4)線程之間可以共享運行結果。但是這樣做有一定的危險,比如兩個線程更新同一個數據,但是這兩個線程運行的結果不一樣,這叫做競態條件,這個會造成競爭危害,會發生不可預測的結果。所以利用鎖機制可以保護數據。

    Python中的多線程

        python的多線程並沒有想象中的那麼理想,是因爲有一個叫GIL的東西在限制。那什麼是GIL呢?GIL中文名叫全局解釋器鎖,是python虛擬機上用作互斥線程的一種機制,它的作用就是要保證在任何情況下虛擬機上只有一個線程被運行,而其它線程都處在等待GIL鎖被釋放的狀態。所以它是個“僞多線程",它的情況就跟上面說的在單處理器機器上運行多線程一樣,不會加速代碼的執行速度,甚至會增加一些線程管理的開銷。

        python虛擬機上多線程是按如下方式執行的:

            a、設置 GIL

            b、切換到一個線程去運行;

            c、運行指定數量的字節碼指令或者線程主動讓出控制(可以調用 time.sleep(0));

            d、把線程設置爲睡眠狀態;

            e、解鎖 GIL;

            f、再次重複以上所有步驟


        在調用外部代碼(如 C/C++擴展函數)的時候,GIL將會被鎖定,直到這個函數結束爲止(由於在這期間沒有Python的字節碼被運行,所以不會做線程切換)。比如帶有I/O操作(會調用內建的操作系統C代碼,I/O操作就是輸入輸出操作,要想詳細瞭解它可以參考其它資料)的線程,GIL會在這個I/O操作被調用之前就被釋放。

        對於純計算的程序,沒有I/O操作,解釋器會根據sys.ssetcheckinterval()的設置來自動進行線程間的切換,默認情況下是每隔100個時鐘就會釋放GIL鎖從而輪換到其它線程執行。


    那爲什麼Python中還要在多線程中引入GIL呢?是爲了保證對虛擬機內部共享資源訪問的互斥性。python對象的對象管理與引用計數器密切相關,當計數器的值爲0,該對象會被垃圾回收器回收(不瞭解這塊知識的可以在網上查找相關資料或者看《Python源碼解析》這本書),當撤銷對一個對象的引用時,python解釋器會對該對象以及其計數器管理進行以下兩步操作:

a.使引用計數器減1

b.判斷計數器的值是否爲0,如果爲0,則銷燬該對象


    假設現在有A、B兩個線程同時引用同一個對象obj,這時obj對象的引用計數器的值就爲2,如果現在A線程打算撤銷對obj的引用,當執行完第一步”使引用計數器值減1“的時候,由於存在多線程調度機制,A恰好在這個關鍵點被掛起了,而進入了B線程執行的狀態,如果這個時候B線程也是要撤銷對obj的引用,並且完成了上面的a,b兩個步驟,這時obj的引用計數器就是0了,obj對象就被銷燬了,內存被釋放出來了,麻煩就可能出現了,當A線程再次被喚醒時,它肯定會接着執行上面的b步驟,結果發現已經面目全非了,那麼其操作結果完全未知。所以引入了GIL,保證對虛擬機內部共享資源訪問的互斥性。


    GIL的引入使多線程不能在多核系統中發揮優勢,但也帶來了一些好處,就是大大簡化了Python線程中共享資源的管理。不過Python提供了其它方式繞過了GIL的侷限性來充分利用多核的計算能力,比如多進程multiprocessing模塊、C語言擴展方式、ctypes庫等等。



(二)進程

     進程(有時被稱爲重量級進程)是程序的一次執行。每個進程都有自己的地址空間、內存、數據棧以及其它記錄其運行軌跡的輔助數據。操作系統管理在其上運行的所有進程,併爲這些進程公平地分配時間。進程也可以通過fork和spawn操作來完成其它的任務,不過各個進程有自己的內存空間、數據棧等,所以只能使用進程間通訊(IPC),而不能直接共享信息。


(三)協程

(1)協程是一種用戶級的輕量級線程,不同於線程的地方在於協程不是操作系統進行切換,而是由程序員編碼進行切換的,也就是說切換是由程序員控制的,這樣就沒有了線程所謂的安全問題。

 (2)協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。

  

    python裏面怎麼使用協程?答案是使用gevent模塊。使用協程,可以不受線程開銷的限制。所以最推薦的方法,是多進程+協程(可以看作是每個進程裏都是單線程,而這個單線程是協程化的)多進程+協程下,避開了CPU切換的開銷,又能把多個CPU充分利用起來。


        以上是對線程、進程及協程的理解,希望對你有幫助!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章