Python編程(bbb二):Python進程、線程那點事兒

多進程,多線程編程

系統程序員、運維開發程序員在面試的時候經常會被問及一個常見問題:

進程是什麼,線程是什麼,進程和線程有什麼區別?

不得不承認,這麼多年了。這個問題依舊是個很難以招架的問題,簡單地說:

進程和線程有很多類似的性質,他們都可以被CPU作為一個單元進行調度,他們都擁有自己獨立的棧(stack)等等。因此線程也被稱作LWP(Lightweight Process,輕量級進程);對應的進程也可以被稱作為HWP(Heavyweight Process,重量級進程),從線程的角度看,進程就是只有一個線程的進程。如果一個進程有多個線程,那麼它就能同時(只有在SMT系統中才有可能真正的「同時」)執行多個任務。他們的異同可以從以下幾個角度來論述:

調度

在傳統的計算機操作系統中,CPU調度的基本單位是進程。後來操作系統普遍引入了線程的概念,線程成為了CPU調度的基本單位,進程只能作為資源擁有的基本單位。

並行

由於線程的引入,原先一個進程只能有一個並發,現在一個進程可以有多個線程並行執行。早期的很多HTTP server都是通過線程來解決伺服器的並發,比起之前用fork子進程來處理並發效率有了數倍的提升。這一切都得益於線程可以用進程更低的代價實現並發。

共享

一般來說Linux線程會繼承或共享如下資源:

  • 進程代碼段,如下圖所示

  • 進程的公有數據段內存(利用這些共享的數據,線程很容易的實現相互之間的通訊)
  • 進程打開的FD(File Descriptor,文件描述符)
  • 信號的處理器
  • 進程用戶ID(UID)與進程組ID(PGID)

隔離

Linux的線程會獨立擁有如下資源(非共享):

  • 線程ID,在Linux中線程和進程共享ID空間,在UNIX系統中線程的ID是和進程ID不同層面的概念
  • 寄存器的值,這其實就是線程能作為獨立調度單元的最必要的保證
  • 線程的棧,這是線程能並行運行的保證
  • 優先順序,Linux的系統設計使得線程和進程除了在某些資源的共享&隔離有差異之外, 幾乎是一視同仁的,所以他們可以有不同的priority。

多進程多線程的產生,在Linux系統中的地位

Linux由於從一開始的定位就是一個多任務操作系統,從Linus Torvalds寫出第一個版本的時候就有了進程的概念。比如我們耳熟能詳的init進程的pid就是1。

線程的產生是為了解決並發問題,線程的定位也是輕量級的進程。

Linux內核在2.6版本之前都是沒有線程的概念的,任務的最小調度單元都是進程。但Linux 在設計的時候就為線程的引入創造了良好的條件,Linux中著名的啟動新進程系統調用fork就是通過內核調用clone實現的拷貝地址空間等資源。Linux通過改變內核調用clone的參數就很簡單的創造出了線程。所以,從現代操作系統內核的調度的角度來說,進程和線程的差異微乎其微。

但不幸的是Linux早期的內核版本通過細微修改增加的線程機制和POSIX標準並不完全兼容,特別是信號處理、調度、跨進程同步的行為上。

為了推進Linux Threads和POSIX標準的統一,兩撥人做了很多的努力:IBM牽頭的NGPT (Next Generation POSIX Threads)和紅帽(Red Hat)主推的NPTL(Native POSIX Thread Library)。這場競爭以NPTL的勝利告終,NPTL的用戶態API就是我們現在常用的Pthread系列API。這場Red Hat戰勝IBM的戰爭也基本確立了前者在Linux界扛把子的地位。

在NPTL成為Linux的POSIX事實標準之前,以FreeBSD為首的UNIX系統保持了對Linux的性能優勢。這也就導致了很多歷史比較老的公司當年系統都用的FreeBSD而不是Linux。

為什麼不能一味的開線程解決並發問題

上面說到,線程的出現是為了解決Linux系統面臨的日益增多的,並發編程的需求。

但就像我們這一小節的標題講的一樣:「不能一味的開線程解決並發問題」。

這是由於上下文切換(Context Switch)的代價:當計算機還處於單核時代的時候 ,就已經有了多任務操作系統。但單核的CPU在同一時刻只能運行一個進程的一個指令。 為了達到用戶想要的「多任務」同時運行(比如,我在敲這段文字的時候,後台還在運行著 iTunes播放音樂,還有一個迅雷在我的虛擬機里運行)。Linux通過把CPU的時間切成 大小不等的時間片,通過內核的調度演算法,讓他們輪流來佔用寶貴的CPU資源。由於切換的 時間片的大小一般都是微秒,所以在我們人類看來,計算機就在運行「多任務」。

上下文切換(Context Switch)

一個程序如果運行到了他的時間片結束還沒有完成他的工作,那麼,對不起,請把你需要 保存的數據(通常是一些CPU寄存器的數值)存儲在內存中,然後排隊去吧。

什麼,稍等?NO,NO,NO 這不是一個用戶態的進程能夠和內核討價還價的。

保存這個現場是需要一定的代價的,更嚴重的是,這將極大的影響CPU的分支預測, 影響系統性能。所以上下文切換(Context Switch)是我們要極力避免的。

進程或者線程開的多了,就會導致上下文切換(Context Switch)增多,嚴重影響 系統性能。

所以:「不能一味的開線程解決並發問題」。

協程(coroutine)簡介

精闢的說,協程就是用戶自己在進程中控制多任務的棧,儘可能的不讓進程由於外部中斷或者IO 的等待喪失CPU調度的時間片,從而在進程內部實現並發。

為了緩解,處理高並發的連接,Linux在很早的時候就引入了協程,來緩解上下文切換造成 的性能損失,在某種程度上實現非同步編程。但由於協程的編程太過於晦澀難懂,所以即便是 協程在線程之前更早的被引入Linux內核,也始終沒有流行起來。

下面是wikipedia對於協程的一段描述,大家可以參考一下:

到2003年,很多最流行的編程語言,包括C和他的後繼,都未在語言內或其標準庫中直接支持協程。(這在很大程度上是受基於堆棧的子常式實現的限制)。

有些情況下,使用協程的實現策略顯得很自然,但是此環境下卻不能使用協程。典型的解決方法是創建一個子常式,它用布爾標誌的集合以及其他狀態變數在調用之間維護內部狀態。代碼中基於這些狀態變數的值的條件語句產生出不同的執行路徑及後繼的函數調用。另一種典型的解決方案是用一個龐大而複雜的switch語句實現一個顯式狀態機。這種實現理解和維護起來都很困難。

在當今的主流編程環境里,線程是協程的合適的替代者,線程提供了用來管理「同時」執行的代碼段實時交互的功能。因為要解決大量困難的問題,線程包括了許多強大和複雜的功能並導致了困難的學習曲線。當需要的只是一個協程時,使用線程就過於技巧了。然而——不像其他的替代者——在支持C的環境中,線程也是廣泛有效的,對很多程序員也比較熟悉,並被很好地實現,文檔化和支持。在POSIX里有一個標準的良定義的線程實現pthread.

但近些年來,golang的努力,似乎又讓這個古老的機制有了復甦的跡象。

程序運行時的內存布局

首先我們需要了解一個基礎知識:程序運行時的內存,也就是我們在用戶態能看到的內存地址,都不是物理內存中的地址。現代操作系統都會在物理內存上做一層內存映射(memory mapping)。所以如下圖,每個進程的內存空間都是獨立的,0x8000這種地址在物理地址中其實是不一樣的地址。

如上圖,每個線程,都有自己獨立的「棧」、「寄存器」、「線程計數器」。每個進程可以有多個線程。 同一個進程里的線程都可以共享內存空間。

多進程和多線程的選用場景

在Linux系統編程中,多進程和多線程都有自己的用武之地。

多數情況他們的選用是按照他們的特性,其中最重要的特性就是上面提過的「共享」、「隔離」。

我們舉個例子來說吧:

我們所熟知的memcached,是個典型多線程編程。之所以他是多線程,而不是多進程 主要的一個原因在於,memcached的多個線程需要共享內存中的Key-Value數據。所以多線程 是一個必然的選擇。

然後就是大名鼎鼎的Nginx,是個典型的多進程編程。由於Nginx所要處理的HTTP請求都是 比較獨立的,沒有太多需要共享的數據。更重要的是Nginx需要支持「不停服務重啟server」這一特性 這個功能也是這能在多進程框架下才能實現的。

所以,一個結論就是:到底是多進程好,還是多線程需要根據業務場景來分析選擇。

Python的GIL

GIL是Global Interpreter Lock的縮寫。顧名思義,就是Python解釋器的一個全局鎖。 它的產生是由於Python解釋器在實現的時候作者為了「糙快猛」地實現出一個原型引入了很多 全局變數,由於全局變數的存在就要加鎖,為了加鎖那乾脆一不做二不休,加個全局鎖吧…… 嗯,當時情況應該就是這樣的。

後來Python逐漸流行起來,很多模塊的作者一方面也是為了簡化問題,另一方面也是由於Python解釋器 本身就有GIL,很多模塊自己也肆無忌憚地引入了很多全局變數。

從此Python的GIL就走上了一條不歸路,對Python程序員的影響就是,Python的多線程在同一時刻 只能有一個線程在運行。多線程情況下就是線程不停地在搶鎖,搶得頭破血流。

關於Python的GIL及其造成的性能影響,這篇David Beazley的這篇文章做了非常深刻的論述:

Global Interpretor Lock

這部分的內容我們將在課上做更加深入的論述。

我們可以得到的結論:

  • Python的多線程對CPU密集型是反作用,對IO密集型可以採用
  • Python多進程能充分利用多核CPU

推薦閱讀:

TAG:Python | 多线程 | 并发 |