併發技術、進程、線程和鎖拾遺

joakim-honkasalo-tvVcBCFVHY8-unsplash

併發技術、進程、線程和鎖拾遺

Part1. 多任務

計算機發展起初,CPU 資源十分昂貴,若是讓 CPU 只能運行一個程序那麼當 CPU 空閒下來(例如等待 I/O 時),CPU 資源就會被浪費,爲了使 CPU 資源獲得更好的利用,先驅編寫了一個監控程序,若是發現某個程序暫時無需使用 CPU 時,監控程序就把另外的正在等待 CPU 資源的程序啓動起來,以充分利用 CPU資源。這種方法稱爲 - 多道程序(Multiprogramming)程序員

對於多道程序,最大的弊端是各程序之間不區分輕重緩急,對於用戶交互式的程序來講,對 CPU 計算時間的需求並很少,可是對於響應速度卻有比較高的要求。而對於計算類程序來講則相反,對響應速度要求低,但須要長時間的 CPU 計算。想象一個場景:我在同時在瀏覽網頁和聽音樂,咱們但願瀏覽器可以快速響應,同時也但願音樂不停,這時候多道程序就無法達到咱們的要求了。算法

因而人們改進了多道程序,使得每一個程序運行一段時間以後,都主動讓出 CPU 資源,這樣每一個程序在一段時間內都有機會運行一小段時間。這樣像瀏覽器這樣的交互式程序就可以快速地被處理,同時計算類程序也不會受到很大影響。這種程序協做方式被稱爲 分時系統(Time-Sharing System)瀏覽器

在分時系統的幫助下,咱們能夠邊用瀏覽器邊聽歌了。可是若是某個程序出現了錯誤,致使了死循環,不只僅是這個程序會出錯,整個系統都會死機,爲了不這種狀況,一個更爲先進的操做系統模式被髮明處理,也就是咱們如今熟悉的多任務系統(Multi-tasking System)服務器

操做系統從最底層接管了全部硬件資源。全部的應用程序在操做系統上以 進程(Process) 的方式運行,每一個進程都有本身獨立的地址空間,相互隔離。CPU 由操做系通通一統一進行分配。每一個進程都有機會獲得 CPU,同時在操做系統控制下,若是一個進程運行超過了必定時間,就會被暫停掉,失去 CPU 資源。這樣就避免了一個程序的錯誤致使整個系統死機。若是操做系統分配給各個進程的運行時間都很短,CPU 能夠在多個進程間快速切斷,就像不少進程都同時在運行的樣子。幾乎全部現代操做系統都是採用這樣的方式支持多任務。微信

Part2. 進程

進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操做系統結構的基礎。它能夠申請和擁有系統資源,是一個活動的實體。它不僅是程序的代碼,還包括當前的活動,經過程序計數器的值和處理遞存器的內容來表示。網絡

  • 進程是一個實體,每個進程都有它本身的地址空間,通常狀況下,包括文本區域(text region)、數據區域(data region)和堆棧(stack region)。文本區域存儲器執行的代碼;數據區域存儲變量和進程執行期間使用的動態分配的內存;堆棧區域存儲着活動過程調用的指令和本地變量。
  • 進程是一個「執行中的程序」。程序是一個沒有生命的實體,只有處理器賦予程序生命時,它才能成爲一個活動的實體,咱們稱其爲進程。

1. 進程的基本狀態

  1. 等待態:等待某個事件的完成;
  2. 就緒態:等待系統分配處理器以便運行;
  3. 運行態:佔有處理器正在運行;

幾種狀態的切換:多線程

  • 運行態 -> 等待態:每每是因爲等待外設,等待主存等資源分配或等待人工干預而引發的。
  • 等待態 -> 就緒態:則是等待的條件已知足,只需分配處處理器後就能運行。
  • 運行態 -> 就緒態:不是因爲自身緣由,而是由外界緣由使運行狀態的進程讓出處理器,這時就變成就緒態。例如:時間片用完,或有更高優先級的進程來搶佔處理器等
  • 就緒態 -> 運行態:系統按某種策略選中就緒隊列中的一個進程佔用處理器,此時就變成了運行態。

2. 進程調度

調度種類

高級、中級和低級調度做業從提交開始直到完成,每每要經歷下述三級調度:併發

  • 高級調度(High-Level Scheduling):又稱爲做業調度,它決定把後備做業調入內存運行;
  • 中級調度(Intermediate-Level Scheduling):又稱爲在虛擬存儲器中引入,在內、外存對換區進行進程對換。
  • 低級調度(Low-Level Scheduling):又稱爲進程調度,它決定把就緒隊列的某進程得到 CPU。

非搶佔式調度與搶佔式調度

  • 非搶佔式
    分派程序一旦把處理機分配給某進程後便讓它一直運行下去,直到進程完成或者發生進程調度某事件而阻塞時,才把處理機分配給另外一個進程。
  • 搶佔式
    操做系統將正在運行的進程強行暫停,由調度程序將 CPU 分配給其餘就緒進程的調度方式。

調度策略的設計

  • 響應時間:從用戶輸入到產生反應的時間
  • 週轉時間:從任務開始到任務結束的時間

CPU 任務能夠分爲交互式任務和批處理任務,調度最終的目標是合理的使用 CPU,使得交互式任務的響應時間儘量短,用戶不至於感到延遲,同時使得批處理任務的週轉時間儘量短,減小用戶等待的時間。函數

調度算法

  1. FIFO 或 First Come,First Served(FCFS)
    調度的順序就是任務到達就緒隊列的順序。
    公平、簡單(FIFO 隊列)、非搶佔、不適合交互式。未考慮任務特性,平均等待時間能夠縮短。性能

  2. Shortest Job First(SJF)
    最短的做業(CPU 區間長度最小)優先調度
    能夠證實,SJF 能夠保證最小的平均等待時間
    Shortest Job First (SRJF): SJF 的可搶佔版本,比 SJF 更有優點
    SJF、SRJF 如何知道下一 CPU 區間大小?根據歷史進行預測:指數平均法。

  3. 優先權調度
    每一個任務關聯一個優先權、調度優先權最高的任務。
    注意:優先權過低的任務一直就緒,得不到運行,出現「飢餓」現象。
    FCFS 是 RR 的特例,SJF 是優先權調度的特例,這些調度算法都不適合於交互式系統。

  4. Round-Robin(RR)
    設置一個時間片,按時間片來輪轉調度(「輪叫」算法)
    優勢:定時有響應,等待時間較短;缺點:上下文切換次數較多;
    如何肯定時間片?時間片太大,響應時間太長;吞吐量變小,週轉時間變長;當時間片過長時,退化爲 FCFS。

  5. 多級隊列調度
    按照必定的規則創建多個進程隊列
    不一樣的隊列有固定的優先級(高優先級有搶佔權)
    不一樣的隊列能夠給不一樣的時間片和採用不一樣的調度方法
    存在問題 1:無法區分 I/O bound 和 CPU bound;
    存在問題 2:也存在必定程度的「飢餓」現象

  6. 多級反饋隊列
    在多級隊列的基礎上,任務能夠在隊列之間移動,更細緻的區分任務。
    能夠根據「享用」CPU 時間多少來移動隊列,阻止「飢餓」。
    最通用的調度算法,多數 OS 都使用該方法或其變形,如 UNIX、Windows 等。

3. 進程同步

臨界資源與臨界區

在操做系統中,進程是佔有資源的最小單位(線程能夠訪問其所在進程內的全部資源,但線程自己並不佔有資源或僅僅佔有一點必須資源)。但對於某些資源來講,其在同一時間只能被一個進程所佔用。這些一次只能被一個進程所佔用的資源就是所謂的臨界資源。
典型的臨界資源好比物理上的打印機,或是存在硬盤或內存中被多個進程所共享的一些變量和數據等(若是這類資源不被當作臨界資源加以保護,那麼頗有可能形成丟數據的問題)。
對於臨界資源的訪問,必須是互斥進行。也就是當臨界資源被佔用時,另外一個申請臨界資源的進程會被阻塞,直到其所申請的臨界資源被釋放。而進程內訪問臨界資源的代碼被稱爲臨界區。

解決臨界區問題可能的方法:

  1. 通常軟件方法
  2. 關中斷方法
  3. 硬件原子指令方法
  4. 信號量方法

信號量

信號量是一個肯定的二元組(s,q),其中 s 是一個具備非負初值的整型變量,q 是一個初始狀態爲空的隊列,整型變量 s 表示系統中某類資源的數目:

  • 當 s ≥ 0 時,表示系統中當前可用資源的數目
  • 當 s < 0 時,其絕對值表示系統中因請求該類資源而被阻塞的進程數目

除信號量的初值外,信號量的值僅能由 P 操做和 V 操做更改,操做系統利用它的狀態對進程和資源進行管理。

P 操做:P 操做記爲 P(s),其中 s 爲一信號量,它執行時主要完成如下動做:

// 可理解爲佔用一個資源,若原來就沒有則記帳「欠」1 個
s.value = s.value - 1;

s.value ≥ 0,則進程繼續執行,不然(即s.value < 0),則進程被阻塞,並將該進程插入到信號量 s 的等待隊列 s.queue 中。
實際上,P 操做能夠理解爲分配資源的計數器,或是使進程處於等待狀態的控制指令

V 操做:V 操做記爲 V(s),其中 s 爲一信號量,它執行時,主要完成如下動做:

// 可理解爲歸還一個資源,若原來就沒有則意義是用此資源還 1 個欠帳
s.value = s.value + 1;

s.value > 0,則進程繼續執行,不然(即 s.value ≤ 0),則從信號量 s 的等待隊列 s.queue 中移出第一個進程,使其變爲就緒狀態,而後返回原進程繼續執行。
實際上,V 操做能夠理解爲歸還資源的計數器,或是喚醒進程使其處於就緒狀態的控制指令

信號量方法實現:生產者 - 消費者互斥與同步控制

semaphore fullBuffers = 0;//倉庫中已填滿的貨架個數
semaphore emptyBuffers = BUFFER_SIZE;//倉庫貨架空閒個數
semaphore mutex = 1;//生產 - 消費互斥信號

Producer() 
{ 
    while(True)
    {  
       /*生產產品item*/
       emptyBuffers.P(); 
       mutex.P(); 
       /*item存入倉庫buffer*/
       mutex.V();
       fullBuffers.V();
    }
}
 
Consumer() 
{
    while(True)
    {
        fullBuffers.P(); 
        mutex.P();  
        /*從倉庫buffer中取產品item*/
        mutex.V();
        emptyBuffers.V();
        /*消費產品item*/
    }
}

死鎖

死鎖:多個進程因循環等待而形成的沒法執行的現象
死鎖會形成進程沒法執行,同時會形成系統資源的極大浪費(資源沒法釋放)。
死鎖產生的 4 個必要條件:

  • 互斥使用(Mutual exclusion)
    • 指進程對全部分配到的資源進行排他性使用,即在一段時間內某資源只由一個進程佔用。若是此時還有其餘進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。
  • 不可搶佔(No preemption)
    • 指進程已得到的資源,在未使用完以前,不能被剝奪,只能在使用完時由本身釋放。
  • 請求和保持(Hold and wait)
    • 指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其餘進程佔有,此時請求進程阻塞,但又對本身已得到其餘資源保持不放。
  • 循環等待(Circular wait)
    • 指在發生死鎖時,必然存在一個進程-資源的環形鏈,即進程集合{P0, P1, P2, P3, P4, ..., Pn} 中的 P0 正在等待一個 P1 佔用的資源;P1 正在等待 P2 佔用的資源,...,Pn 正在等待已被 P0 佔用的資源。

死鎖的避免:銀行家算法
思想:判斷這次請求是否形成死鎖,若會形成死鎖,則拒絕該請求。

4. 進程間通訊

本地進程間通訊的方式有不少,能夠總結爲下面四類:

  • 消息傳遞(管道、FIFO、消息隊列)
  • 同步(互斥量、條件變量、讀寫鎖、文件和寫記錄鎖、信號量)
  • 共享內存(匿名的和具名的)
  • 遠程過程調用(Solaris門 和 Sun RPC)

Part3. 線程

多線程解決了前面提到的多任務問題。然而不少時候不一樣的程序須要共享一樣的資源(文件,信號量等),若是全都使用進程的話會致使切換的成本很高,形成 CPU 資源的浪費。因而出現了線程的概念。
線程,有時被稱爲輕量級進程(Lightweight Process,LWP),是程序執行流的最小單元。一個標準的線程由線程 ID,當前指令指針(PC),寄存器集合和堆棧組成。
線程具備如下屬性:

  1. 輕型實體
    線程中的實體基本上不擁有系統資源,只是有一點必不可少的、能保證獨立運行的資源。線程的實體包括:程序、數據和 TCB(Thread Control Block)。線程是動態概念,它的動態特性由線程控制塊 TCB 描述。

  2. 獨立調度和分派的基本單位
    在多線程 OS 中,線程是能獨立運行的基本單位,於是也是獨立調度和分派的基本的單位。因爲線程很「輕」,故線程的切換很是迅速且開銷小(在同一進程中的)

  3. 可併發執行
    在一個進程中的多個線程之間,能夠併發執行,甚至容許在一個進程中全部線程都能併發執行;

  4. 共享進程資源
    在同一進程中的各個線程,均可以共享該進程所擁有的資源,這首先表如今:全部線程都具備相同的地址空間(進程的地址空間),這意味着,線程能夠訪問改地址空間的每個虛擬地址;此外,還能夠訪問進程所擁有的已打開文件、定時器、信號量等。因爲同一個進程內的線程共享內存和文件,因此線程之間互相通訊沒必要調用內核。
    線程共享的環境包括:進程代碼段、進程的公有數據(利用這些共享的數據,線程很容易的實現相互以前的通信)、進程打開的文件描述符、信號的處理器、進程的當前目錄和進程用戶 ID 與進程組 ID。

Part4. 鎖

鎖要解決的是線程之間爭奪資源的問題:

  • 資源是不是獨佔(獨佔鎖 - 共享鎖)
  • 搶佔不到資源怎麼辦(互斥鎖 - 自旋鎖)
  • 本身能不能重複搶(重入鎖 - 不可重入鎖)
  • 競爭讀的狀況比較多,讀可不能夠不加鎖(讀寫鎖)

上面幾個角度並不是相互獨立,在實際場景中須要將他們集合起來才能構造出一個合適的鎖。

獨佔鎖 - 共享鎖

當一個共享資源只有一份的時候,一般咱們使用獨佔鎖,常見的即各個語言中的 Mutex。當共享資源有多份時,可使用信號量(Semaphere)。

互斥鎖 - 自旋鎖

對於互斥鎖來講,若是一個線程已經鎖定了一個互斥鎖,第二個線程又試圖去獲取這個互斥鎖,則第二個線程將會被掛起(即休眠、不佔用 CPU 資源)。

在計算機系統中,頻繁的掛起和切換線程,也是有成本的。自旋鎖就是解決這個問題的。

自旋鎖:指當一個線程在獲取鎖的時候,若是鎖已經被其餘線程獲取,那麼該線程將循環等待,而後不斷的判斷鎖是否可以被成功獲取,直到獲取到鎖纔會退出循環。

容易看出,當資源等待的時間較長,用互斥鎖讓線程休眠,會消耗更少的資源,當資源等待的時間較短時,使用自旋鎖將減小線程的切換,得到更高的性能。

Java 中的 synchornized 和 .NET 中的 lockMonitor)的實現,是結合了兩種鎖的特色。簡單說,它們在發現資源被搶佔以後,會先試着自旋等待一段時間,若是等待時間太長,則會進入掛起狀態。經過這樣的實現,能夠較大程度上挖掘出鎖的性能。

重入鎖 - 不可重入鎖

可重入鎖(ReetrantLock),也叫做遞歸鎖,指的是同一線程內,外層函數得到鎖以後,內層遞歸函數仍然能夠獲取到該鎖。
換而言之:同一線程再次進入同步代碼時,可使用本身已獲取到的鎖。

使用可重入鎖時,在同一線程中屢次獲取鎖,不會致使死鎖。使用不可重入鎖,則會致使死鎖發生。

Java 中的 synchornized 和 .NET 中的 lockMonitor) 都是可重入的。

讀寫鎖

有些狀況下,對於共享資源讀競爭的狀況遠遠多於寫競爭,這種狀況下,對讀操做每次都進行加鎖,是得不償失的。讀寫鎖就是爲了解決這個問題。

讀寫鎖容許同一時刻被多個讀線程訪問,可是在寫線程訪問時,全部讀線程和其餘的寫線程都會被阻塞。簡單能夠總結爲,讀讀不互斥,讀寫互斥,謝謝互斥。

對讀寫鎖來講,有一個升級和降級的概念,即當前得到了讀鎖,想把當前的鎖變成寫鎖,成爲升級,反之稱爲降級。鎖的升降級自己也是爲了提高性能,經過改變當前鎖的性質,避免重複獲取鎖。

Part.5 協程

協程,又稱爲微線程,纖程。英文名: Coroutine
協程能夠理解爲用戶級線程,協程和線程的區別是:線程是搶佔式的調度,而協程是協同式的調度,協程避免了無心義的調度,由此能夠提升性能,但也所以,程序員必須本身承擔調度的責任,同時,協程也失去了標準線程使用多 CPU 的能力。

Part.6 IO多路複用

基本概念

IO 多路複用是指內核一旦發現進程指定的一個或者多個 IO 條件準備讀取,他就通知該進程。IO 多路複用適用於以下場景:

  1. 當客戶處理多個描述字時(通常是交互式輸入和網絡套接口),必須使用 I/O 複用。
  2. 當一個客戶同時處理多個套接口時,而這種狀況是可能的,但不多出現。
  3. 若是一個 TCP 服務器既要處理監聽套接口,又要處理已鏈接套接口,通常也要用到 I/O 複用。
  4. 若是一個度武器既要處理 TCP,又要處理 UDP,通常要使用 I/O 複用。
  5. 若是一個服務器要處理多個服務或多個協議,通常要使用 I/O 複用。

與多進程和多線程技術相比,I/O 多路複用技術的最大優點是系統開銷小,系統沒必要建立 進程/線程,也沒必要維護這些 進程/線程,從而大大減少了系統的開銷。

常見的 IO 複用實現

  • select (Linux/Windows/BSD)
  • epoll (Linux)
  • kqueue (BSD/Mac OS)

更多幹貨文章

博客:www.qiuxuewei.com
微信公衆號:@開發者成長之路
公衆號二維碼

一個沒有雞湯只有乾貨的公衆號 ****************************************************

相關文章
相關標籤/搜索