簡而言之,所謂併發編程是指在一臺處理器上「同時」處理多個任務。linux
隨着硬件的發展,併發程序變得愈來愈重要。Web服務器會一次處理成千上萬的請求。平板電腦和手機app在渲染用戶畫面同時還會後臺執行各類計算任務和網絡請求。即便是傳統的批處理問題--讀取數據,計算,寫輸出--如今也會用併發來隱藏掉I/O的操做延遲以充分利用現代計算機設備的多個核心。計算機的性能每一年都在以非線性的速度增加。git
宏觀的併發是指在一段時間內,有多個程序在同時運行。編程
併發在微觀上,是指在同一時刻只能有一條指令執行,但多個程序指令被快速的輪換執行,使得在宏觀上具備多個進程同時執行的效果,但在微觀上並非同時執行的,只是把時間分紅若干段,使多個程序快速交替的執行。windows
並行: 並行(parallel):指在同一時刻,有多條指令在多個 CPU 處理器上同時執行。安全
併發(concurrency): 指在同一時刻只能有一條指令執行,但多個進程指令被快速的輪換執行。使得在宏觀上具備多個進程同時執行的效果,但在微觀上並非同時執行的,只是把時間分紅若干段,經過 cpu 時間片輪轉使多個進程快速交替的執行。bash
大師曾以咖啡機的例子來解釋並行和併發的區別:服務器
程序,是指編譯好的二進制文件,只佔用磁盤空間,不佔用系統資源(cpu、內存、打開的文件、設備、鎖 ...)網絡
進程,是一個抽象的概念,與操做系統原理聯繫緊密。進程是活躍的程序,佔用系統資源。在內存中執行。(程序運行起來,產生一個進程)數據結構
程序 → 劇本(紙) 進程 → 戲(舞臺、演員、燈光、道具 ...)多線程
同一個劇本能夠在多個舞臺同時上演。一樣,同一個程序也能夠加載爲不一樣的進程(彼此之間互不影響)
如:同時開兩個終端。各自都有一個 bash 但彼此 ID 不一樣。
在 windows 系統下,經過查看「任務管理器」,能夠查看相應的進程。運行起來的程序就是一個進程。以下圖所示:
進程基本的狀態有5種。分別爲初始態,就緒態,運行態,掛起(阻塞)態與終止(中止)態。其中初始態爲進程準備階段,常與就緒態結合來看。
在使用進程 實現併發時會出現什麼問題呢?
經過前面查看操做系統的進程信息,咱們知道在操做系統中,能夠產生不少的進程。
在 unix/linux 系統中,正常狀況下,子進程是經過父進程 fork 建立的,子進程再建立新的進程。
而且父進程永遠沒法預測子進程到底何時結束。 當一個 進程完成它的工做終止以後,它的父進程須要調用系統調用取得子進程的終止狀態。
孤兒進程:父進程先於子進程結束,則子進程成爲孤兒進程,子進程的父進程成爲 init 進程,稱爲 init 進程領養孤兒進程。
殭屍進程:子進程終止,父進程還沒有回收,子進程殘留資源(PCB)存放於內核中,變成殭屍(Zombie)進程。
Windows 下的進程和 Linux 下的進程是不同的,它比較懶惰,歷來不執行任何東西,只是爲線程提供執行環境。而後由線程負責執行包含在進程的地址空間中的代碼。當建立一個進程的時候,操做系統會自動建立這個進程的第一個線程,成爲主線程。
LWP:light weight process 輕量級的進程,本質還是進程 (Linux下)
進程:獨立地址空間,擁有 PCB
線程:有獨立的 PCB,但沒有獨立的地址空間(共享)
區別:在因而否共享地址空間。獨居(進程);合租(線程)。
進程:最小分配資源單位,可當作是隻有一個線程的進程。
線程:最小的執行單位
Windows 系統下,能夠直接忽略進程的概念,只談線程。由於線程是最小的執行單位,是被系統獨立調度和分派的基本單位。而進程只是給線程提供執行環境。
同步即協同步調,按預約的前後次序運行。
線程同步,指一個線程發出某一功能調用時,在沒有獲得結果以前,該調用不返回。同時其它線程爲保證數據一致性,不能調用該功能。
舉例1: 銀行存款 5000。櫃檯:取3000;同時提款機:取 3000。剩餘:2000。
舉例2: 內存中 100 字節,線程T1欲填入全1, 線程T2欲填入全0。但若是T1執行了50個字節失去cpu,T2執行,會將T1寫過的內容覆蓋。當T1再次得到cpu繼續 從失去cpu的位置向後寫入1,當執行結束,內存中的100字節,既不是全1,也不是全0。
產生的現象叫作「與時間有關的錯誤」(time related)。爲了不這種數據混亂,線程須要同步。
「同步」的目的,是爲了不數據混亂,解決與時間有關的錯誤。實際上,不只線程間須要同步,進程間、信號間等等都須要同步機制。
所以,全部「多個控制流,共同操做一個共享資源」的狀況,都須要同步。
Linux 中提供一把互斥鎖 mutex(也稱之爲互斥量)。
每一個線程在對資源操做前都嘗試先加鎖,成功加鎖才能操做,操做結束解鎖。
資源仍是共享的,線程間也仍是競爭的,但經過「鎖」就將資源的訪問變成互斥操做,然後與時間有關的錯誤也不會再產生了。
可是應注意:同一時刻,只能有一個線程持有該鎖。
當 A 線程對某個全局變量加鎖訪問,B 在訪問前嘗試加鎖,拿不到鎖,B 阻塞。C 線程不去加鎖,而直接訪問該全局變量,依然可以訪問,但會出現數據混亂。
因此,互斥鎖實質上是操做系統提供的一把「建議鎖」(又稱「協同鎖」),建議程序中有多線程訪問共享資源的時候使用該機制。可是並無強制限定。
所以,即便有了 mutex,若是有線程不按規則來訪問數據,依然會形成數據混亂。
與互斥量相似,但讀寫鎖容許更高的並行性。其特性爲:寫獨佔,讀共享。
讀寫鎖狀態:
特別強調:讀寫鎖只有一把,但其具有兩種狀態:
讀寫鎖特性:
讀寫鎖也叫共享-獨佔鎖。當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當它以寫模式鎖住時,它是以獨佔模式鎖住的。寫獨佔、讀共享。
讀寫鎖很是適合於對數據結構讀的次數遠大於寫的狀況。
協程:coroutine。也叫輕量級線程。
與傳統的系統級線程和進程相比,協程最大的優點在於「輕量級」。能夠輕鬆建立上萬個而不會致使系統資源衰竭。而線程和進程一般很難超過1萬個。這也是協程別稱「輕量級線程」的緣由。
一個線程中能夠有任意多個協程,但某一時刻只能有一個協程在運行,多個協程分享該線程分配到的計算機資源。
多數語言在語法層面並不直接支持協程,而是經過庫的方式支持,但用庫的方式支持的功能也並不完整,好比僅僅提供協程的建立、銷燬與切換等能力。若是在這樣的輕量級線程中調用一個同步 IO 操做,好比網絡通訊、本地文件讀寫,都會阻塞其餘的併發執行輕量級線程,從而沒法真正達到輕量級線程自己指望達到的目標。
在協程中,調用一個任務就像調用一個函數同樣,消耗的系統資源最少!但能達到進程、線程併發相同的效果。
協程的目的:提升程序執行的效率。
在一次併發任務中,進程、線程、協程都可以實現。從系統資源消耗的角度出發來看,進程至關多,線程次之,協程最少。
選擇哪種進行併發並無一個統一的答案,須要根據具體狀況具體分析。
最後,舉一個例子幫助你們更好的理解一下三者的區別:
好比說我是一家生產手機的老闆,要經過一條生產線(進程)去生產手機,光有生產線還不夠,還須要工人(線程)來完成手機的生產。
這時只有一條生產線,一個工人,咱們能夠看做是一個單進程、單線程的程序。
接下來我發現這樣效率過低了,我要多招工人,假設招了 50 個工人,也就是有了 50 個線程。
這時有一條生產線,50 個工人,咱們能夠看做是一個單進程、多線程的程序。
再接下來,還想提升效率,我弄 10 條生產線,每條生產線招 50 個工人,那麼就須要 500 個工人。
這時有 10 條生產線,500 個工人,咱們能夠看做是一個多進程、多線程的程序。
可是若是上一個程序沒有完成,接下來的工人都會在等待。那麼就會有工人在這時刷刷微博、逛逛朋友圈什麼的。我心想這不行,我得把這空閒時間利用起來啊。因此,老闆又制定了一條規則:全部工人,若是有空閒時間,都要到我家來搬磚(協程)。
這時有 10 條生產線,500 個工人,工做的同時還去我家幫忙搬磚,咱們能夠看做是一個多進程、多線程、多協程的程序。(我可真是個黑心老闆 ...)
Go 在語言級別支持協程,叫 goroutine。Go 語言標準庫提供的全部系統調用操做(包括全部同步IO操做),都會出讓 CPU 給其餘 goroutine。這讓輕量級線程的切換管理不依賴於系統的線程和進程,也不須要依賴於 CPU 的核心數量。
有人把 Go 比做21世紀的 C 語言。第一是由於 Go 語言設計簡單,第二,21世紀最重要的就是並行程序設計,而 Go 從語言層面就支持並行。同時,併發程序的內存管理有時候是很是複雜的,而 Go 語言提供了自動垃圾回收機制。
Go 語言爲併發編程而內置的上層 API 基於順序通訊進程模型 CSP(communicating sequential processes)。這就意味着顯式鎖都是能夠避免的,由於 Go 經過相對安全的通道發送和接受數據以實現同步,這大大地簡化了併發程序的編寫。
Go 語言中的併發程序主要使用兩種手段來實現。goroutine 和 channel。
歡迎訪問個人我的網站:
李培冠博客:lpgit.com