本文解釋進程、線程與協程是什麼,以及它們的適用狀況,並列舉出實際運用例子。html
本文只介紹進程、線程、協程在 Linux(UNIX) 系統下的狀況。python
進程是正在執行的程序的實例,包括程序計數器、寄存器和變量的當前值。linux
在 Linux 中,進程調用 fork 來建立一個新進程。調用 fork 的稱爲父進程,建立的新進程稱爲子進程。nginx
父子進程擁有各自私有的內存映像,也擁有共享的資源:好比打開的文件、一些只讀的資源等等。若是一個進程修改了本身的私有內存,另外一個是進程沒法看到;若是一個進程修改了可寫的共享資源,那麼另外一個進程看到的是修改後的資源。mongodb
因爲進程數每每大於 CPU 數目,因此 CPU 要在進程之間切換。進程的切換須要保存當前進程的工做,而後加載新進程的工做狀態。數據庫
進程可分爲三種狀態:api
當全部進程處於阻塞態時,CPU 會空轉,從而形成浪費,這種狀況下采用多進程能夠提升 CPU 的利用率。可是因爲建立進程須要消耗其餘資源(好比內存),因此進程數不能無限大,要選擇一個數值使總體資源的消耗率最大。服務器
在傳統操做系統中,每一個進程有一個地址空間和一個控制線程。進程把相關的資源集中起來存放到本身的地址空間中以便管理,而線程則是在 CPU 上被調度執行的實體。進程擁有一個執行的線程,線程中有一個程序計數器,記錄接下來要執行的命令。線程擁有寄存器,用來保存線程當前的工做變量。線程還擁有一個堆棧,用來記錄執行的歷史。網絡
多線程是在一個地址空間中運行的多個控制線程。除了共享地址空間和其餘一些資源,這些線程與進程差很少。多線程
進程中的內容:
線程中的內容:
以上「進程中的內容」是全部線程共享的,而「線程中的內容」是每一個線程獨立的。
使用多線程的主要緣由是它們共享一個地址空間和全部可用數據,而因爲多進程具備不一樣的地址空間,因此多進程不能共享地址空間(可是多進程也能夠經過共享內存來通訊)。因此對於沒有相互之間沒有聯繫的線程,使用多進程單線程;對於相互合做,共同完成一個做業的線程,使用單進程多線程。
因爲線程共用了進程的不少東西,因此線程比進程更加輕量,所以建立、撤銷地更快,在許多系統中,線程比進程的建立快 10 ~ 100 倍。
Linux 的線程是內核線程,因此 Linux 系統的調度是基於線程的,因此多線程也能夠利用多核 CPU。
對一個線程來講是可見的,但對於其餘線程是不可見的。
好比由操做系統維護的 errno
。線程1執行 access
以肯定它是否容許訪問某個文件。操做系統把返回值放到 errno
裏。當控制權返回線程1並在線程1讀取 errno
以前,調度程序確認線程1已用完 CPU 時間,並決定切換到線程2。線程2執行一個 open
調用,結果失敗,致使重寫 errno
,因而給線程1的 errno
會永久丟失。
解決辦法是引入一個新的庫過程,以便建立、設置和讀取這些線程範圍的全局變量。該調用在堆上或線程專用儲存區上分配一個儲存空間用來作這個事情。
好比在 Python 中,有 threading.local()
根據 Difference Between System Call, Procedure Call and Function Call :
當一個用戶程序觸發一個系統調用,會在用戶程序和內核之間發生上下文切換。這意味着用戶程序中止執行並保存它的狀態(推到棧上)。內核被調入以完成任務。結果返回到調用的用戶程序而且內核被調出。
而用戶態線程須要保存信息到該線程的寄存器,查看線程表中的就緒線程,並把新線程的信息裝入寄存器中。只要堆棧指針和程序計數器一被切換,新的線程又自動投入運行。相比陷入內核而言上下文切換要少不少,開銷要小,因此更快,更快的程度是一個數量級或者更多。
那麼既然用戶態更快,爲何不將進程放到用戶態中?
因爲線程共享地址空間,因此第一個問題很好解決,但線程也存在後兩個問題。因此本章中進程的問題、解決方案一樣也適用於線程。
經過進程間的公用儲存區:好比內存或文件。
多個進程讀寫某些共享數據,結果取決於進程的運行順序。
好比 A、B 進程同時向帳戶金額爲 1000 的帳戶增長 100。A 進程讀取到帳戶金額爲 1000,增長 100 後獲得 1100,在將 1100 寫入帳戶前,CPU 調度程序將 A 換下將 B 換上。
B 讀取到帳戶金額爲 1000,加 100 後設置回去,帳戶金額變爲 1100。B 進程執行完畢,切換到 A 進程,A 進程繼續運行,將帳戶金額設置爲 1100,結果帳戶少加了錢。
共享數據進行訪問的程序片斷稱做臨界區。
凡是涉及共享數據的狀況都會引起競爭條件。爲了不這個錯誤,咱們要阻止多個進程同時讀寫共享的數據。即確保,當一個進程在使用一個共享數據的時候,其餘進程不能使用該共享數據。
方法是,當一個進程想進入臨界區時,先檢查是否容許進入,若不容許,則該進程將進入阻塞態,直到其收到臨界區可用的信號。
進程反覆檢查一個條件是否爲真。
忙等待會使 CPU 空轉,浪費 CPU,但若是操做者知道忙等待花費的時間會很短,那麼能夠考慮使用。好比 TSL 的信號量操做,僅需幾個毫秒,這與生產者等待緩衝區騰出是徹底不一樣的時間級別。
用戶線程中,因爲沒有時鐘中止運行時間過長的線程。結果是忙等待的線程將永遠循環下去,毫不會獲得鎖,由於這個運行的線程不會讓其餘線程運行從而釋放鎖。因此線程獲取所獲取鎖失敗後,能夠調用 thread_yield
將 CPU 放棄給另外一個線程,這樣就沒有忙等待,在該線程下次運行時,它將再次對鎖進行測試。
協程和線程相似,有本身的程序計數器、寄存器、堆棧、狀態,和其餘協程共享全局變量等資源。協程和線程的區別是,線程能夠並行而協程是協做的:在任什麼時候刻,只有一個協程在運行。而且一個協程會一直運行除非它主動放棄控制權並將其轉移給另外一個協程。[4]
與線程相比,協程是放棄 CPU 並將其給另外一個協程[2],而線程只是放棄 CPU[3]。
二者均可以屢次放棄 CPU,暫停它們的執行而且從新從暫停處執行,區別是協程能夠馬上控制它放棄後的執行,而生成器不能夠,生成器將控制權還給了生成器的調用者。生成器中的 yield
不會指定要跳到哪一個協程而是將一個值傳回上一級。
uWSGI 能夠設置進程數和線程數。可是並無說明在什麼狀況下適用。
在基於進程或線程處理併發鏈接的模型中,使用單獨的進程或線程來處理每一個鏈接,而且會阻塞在網絡或輸入、輸出操做上。因此 CPU 和內存的利用率低。產生進程或線程須要預先準備運行環境,包括分配內存給堆和棧以及建立新的執行環境。建立這些項目也須要消耗額外的 CPU,而且因爲線程頻繁的上下文切換,最終會致使不好的性能。Apache 就是使用以上模型。
Nginx 採用模塊化、事件驅動、異步、單線程、非阻塞的架構。在一個高效的 run-loop 單線程進程中處理鏈接,這個進程叫作 worker。
因爲 nginx 產生多個 workers 去處理鏈接,因此能夠利用多核 CPU。多個 workers 之間經過共享內存交流。
應該根據磁盤使用和 CPU 使用模式來調整 nginx 的 workers 數量。若是是 CPU 密集型工做,好比處理不少 TCP/IP 鏈接、處理 SSL 或者壓縮,nignx 的 workers 應該與 CPU 核心數一致;若是大部分是 IO 密集型工做,好比從磁盤中提供不少內容,那麼 wokers 數量應該增多,具體取決於 IO 工做的具體狀況,好比等待時間、磁盤儲存類型等。
數據庫鏈接是由客戶端驅動決定的,這裏以 pymongo 和 motor 做爲例子。
每一個 MongoClient 實例都有一個內置的鏈接池。鏈接池按照需求打開 sockets 來支持多線程應用對 MongoDB 的併發操做。
鏈接池的最大數目是 maxPoolSize
,默認爲 100。若是 maxPoolSize
的鏈接正在被使用,那麼下一個請求會一直等待直到有鏈接可用。
MongoClient 實例會打開一個額外的 socket 來監控 MongoDB 服務器的狀態。
不支持多線程和 fork 操做;Motor 只能被用在單線程的應用,好比 Tornado 中。