進程、線程和協程的區別與聯繫

進程

進程就是應用程序的啓動實例。進程擁有代碼和打開的文件資源、數據資源、獨立的內存空間。

進程調度

也叫作業調度,算法包括:

  1. 先來先服務(FCFS,First-Come-First-Served): 按照作業到達後備作業隊列(或進程進入就緒隊列)的先後次序來選擇作業(或進程)。
  2. 短作業優先(SJF,Shortest Process Next):這種調度算法主要用於作業調度,它從作業後備隊列中挑選所需運行時間(估計值)最短的作業進入主存運行。
  3. 時間片輪轉調度算法(RR,Round-Robin):當某個進程執行的時間片用完時,調度程序便停止該進程的執行,並將它送就緒隊列的末尾,等待分配下一時間片再執行。然後把處理機分配給就緒隊列中新的隊首進程,同時也讓它執行一個時間片。這樣就可以保證就緒隊列中的所有進程,在一給定的時間內,均能獲得一時間片處理機執行時間。
  4. 高響應比優先(HRRN,Highest Response Ratio Next): 按照高響應比((已等待時間+要求運行時間)/ 要求運行時間)優先的原則,在每次選擇作業投入運行時,先計算此時後備作業隊列中每個作業的響應比RP,然後選擇其值最大的作業投入運行。
  5. 優先權(Priority)調度算法:按照進程的優先權大小來調度,使高優先權進程得到優先處理的調度策略稱爲優先權調度算法。優先數越多,優先權越小。
  6. 多級隊列調度算法:根據作業的性質和類型的不同,將就緒隊列再分爲若干個子隊列,所有的進程按其性質排入相應的隊列中,不同隊列採用不同的調度算法。

參考:操作系統的進程調度算法

進程間的通信方式

  1. 管道( pipe ):半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關係的進程間使用。進程的親緣關係通常是指父子進程關係。
  2. 有名管道 (named pipe) : 半雙工的通信方式,但是它允許無親緣關係進程間的通信。
  3. 信號量( semophore ) : 信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作爲一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作爲進程間以及同一進程內不同線程之間的同步手段。
  4. 消息隊列( message queue): 消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。
  5. 信號 ( sinal ) : 用於通知接收進程某個事件已經發生。
  6. 共享內存( shared memory ) :共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號量,配合使用,來實現進程間的同步和通信。
  7. 套接字socket:與其他通信機制不同的是,它可用於不同機器間的進程通信。

狀態及轉換

在這裏插入圖片描述

進程同步與互斥

互斥:是指某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的。
同步:是指在互斥的基礎上(大多數情況),通過其它機制實現訪問者對資源的有序訪問。在大多數情況下,同步已經實現互斥,特別是所有寫入資源的情況必定是互斥的。少數情況是指可以允許多個訪問者同時訪問資源。
即,同步體現一種協作性,互斥體現的是一種排他性。

線程

一般程序猿都主要關注線程部分即可。

線程與進程區別

  • 粒度性分析:線程的粒度小於進程。
  • 調度性分析:進程是資源擁有的基本單位,線程是獨立調度與獨立運行的基本單位,除寄存器,程序計數器等必要的資源外基本不擁有其他資源。
  • 系統開銷分析:由於線程基本不擁有系統資源,所以在進行切換時,線程切換的開銷遠遠小於進程。

線程從屬於進程,是程序的實際執行者。一個進程至少包含一個主線程,也可以有更多的子線程。線程擁有自己的棧空間。
無論進程還是線程,都是由操作系統所管理的。對操作系統來說:
進程是資源管理和分配基本單元
線程是調度、執行的基本單元
線程是系統分配處理器時間資源的基本單位?

線程模型

用戶線程位於內核之上,它的管理無需內核支持;而內核線程由操作系統來直接支持與管理。
用戶線程和內核線程對應關係:

  • 多對一模型(用戶級線程模型),線程的創建、調度、同步的所有細節全部由進程的用戶空間線程庫來處理。
    • 優點:用戶線程的很多操作對內核來說都是透明的,不需要用戶態和內核態的頻繁切換,使線程的創建、調度、同步等非常快。
    • 缺點:由於多個用戶線程對應到同一個內核線程,如果其中一個用戶線程阻塞,那麼該其他用戶線程也無法執行;內核並不知道用戶態有哪些線程,無法像內核線程一樣實現較完整的調度、優先級等。
    • 許多語言實現的協程庫基本上都屬於這種方式,比如python的gevent。
  • 一對一模型(內核級線程模型),內核負責每個線程的調度,可以調度到其他處理器上面。
    • 優點:實現簡單。
    • 缺點:對用戶線程的大部分操作都會映射到內核線程上,引起用戶態和內核態的頻繁切換;內核爲每個線程都映射調度實體,線程過多,對性能有影響。
    • Java使用的是一對一線程模型。
  • 多對多模型(兩級線程模型),前兩者的結合,用戶線程一般多於內核線程。
    • 區別於多對一模型,多對多模型中的一個進程可以與多個內核線程關聯,於是進程內的多個用戶線程可以綁定不同的內核線程;
    • 區別於一對一模型,它的進程裏的所有用戶線程並不與內核線程一一綁定,而是可以動態綁定內核線程, 當某個內核線程因爲其綁定的用戶線程的阻塞操作被內核調度讓出CPU時,其關聯的進程中其餘用戶線程可以重新與其他內核線程綁定運行。
    • 所以,多對多模型既不是多對一模型那種完全靠自己調度的也不是一對一模型完全靠操作系統調度的,而是中間態(自身調度與系統調度協同工作),因爲這種模型的高度複雜性,操作系統內核開發者一般不會使用,所以更多時候是作爲第三方庫的形式出現。
    • 優點:兼具多對一模型的輕量;對應多個內核線程,則一個用戶線程阻塞時,其他用戶線程仍然可以執行;對應多個內核線程,則可以實現較完整的調度、優先級等;
    • 缺點:實現複雜
    • goroutine調度器就是採用的這種實現方案,在Go語言中一個進程可以啓動成千上萬個goroutine,自帶高併發。

線程通信方式

  1. 鎖機制:包括互斥鎖、條件變量、讀寫鎖
    互斥鎖提供以排他方式防止數據結構被併發修改的方法。
    讀寫鎖允許多個線程同時讀共享數據,而對寫操作是互斥的。
    條件變量可以以原子的方式阻塞進程,直到某個特定條件爲真爲止。對條件的測試是在互斥鎖的保護下進行的。條件變量始終與互斥鎖一起使用。
  2. 信號量機制(Semaphore):包括無名線程信號量和命名線程信號量
  3. 信號機制(Signal):類似進程間的信號處理
  4. 共享的主內存來
  5. wait方法、notify方法和notifyAll方法

線程間的通信目的主要是用於線程同步,所以線程沒有像進程通信中的用於數據交換的通信機制。

狀態

Java中線程具有五種狀態:初始化、可運行、運行中、阻塞、銷燬。轉化關係如下:
在這裏插入圖片描述
線程不同狀態之間的轉化,需要通過操作系統內核中的TCB(Thread Control Block)模塊來改變線程的狀態,這一過程需要耗費一定的CPU資源。

線程之間是如何進行協作的呢?
如生產者/消費者模式,但性能不高:

  1. 涉及到同步鎖。
  2. 涉及到線程阻塞狀態和可運行狀態之間的切換。
  3. 涉及到線程上下文的切換。

死鎖

進程、線程、協程都會發生死鎖。下面以進程爲例,其他皆適用。

死鎖產生的原因:

  • 競爭資源;
  • 進程推進順序不當。

死鎖產生的必要條件

  1. 互斥條件:一個資源一次只能被一個進程所使用,即是排它性使用。
  2. 不剝奪條件:一個資源僅能被佔有它的進程所釋放,而不能被別的進程強佔。
  3. 請求與保持條件:進程已經保持至少一個資源,但又提出新的資源要求,而該資源又已被其它進程佔有,此時請求進程阻塞,但又對已經獲得的其它資源保持不放。
  4. 環路等待條件:當每類資源只有一個時,在發生死鎖時,必然存在一個進程-資源的環形鏈。

預防死鎖:破壞四個必要條件之一

死鎖的避免:銀行家算法,該方法允許進程動態地申請資源,系統在進行資源分配之前,先計算資源分配的安全性。若此次分配不會導致系統從安全狀態向不安全狀態轉換,便可將資源分配給進程;否則不分配資源,進程必須阻塞等待。從而避免發生死鎖。

死鎖定理:S爲死鎖狀態的充分條件是:尚且僅當S狀態的資源分配圖是不可完全簡化的,該充分條件稱爲死鎖定理。

死鎖解除:

  • 方法1:強制性地從系統中撤消一個或多個死鎖的進程以斷開循環等待鏈,並收回分配給終止進程的全部資源供剩下的進程使用。
  • 方法2:使用一個有效的掛起和解除機構來掛起一些死鎖的進程,其實質是從被掛起的進程那裏搶佔資源以解除死鎖。

線程阻塞

線程阻塞指的是暫停一個線程的執行以等待某個條件發生(如某資源就緒)。Java 提供大量方法來支持阻塞:

  1. sleep()方法:sleep() 允許指定以毫秒爲單位的一段時間作爲參數,它使得線程在指定的時間內進入阻塞狀態,不能得到CPU時間,指定時間一過,線程重新進入可執行狀態。常被用在等待某個資源就緒的情形,測試發現條件不滿足後,讓線程阻塞一段時間後重新測試,直到條件滿足爲止。
  2. suspend() 和 resume() 方法:兩個方法配套使用,suspend()使得線程進入阻塞狀態,並且不會自動恢復,必須其對應的resume() 被調用,才能使得線程重新進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另一個線程產生的結果的情形:測試發現結果還沒有產生後,讓線程阻塞,另一個線程產生結果後,調用 resume() 使其恢復。
  3. wait() 和 notify() 方法:兩個方法配套使用,wait() 使得線程進入阻塞狀態,它有兩種形式,當對應的 notify() 被調用或者超出指定時間時線程重新進入可執行狀態。
  4. yield() 方法:yield() 使得線程放棄當前分得的 CPU 時間,但是不使線程阻塞,即線程仍處於可執行狀態,隨時可能再次分得 CPU 時間。調用 yield() 的效果等價於調度程序認爲該線程已執行足夠的時間從而轉到另一個線程。

協程

簡介

協程,Coroutines,是一種比線程更加輕量級的存在。正如一個進程可以擁有多個線程一樣,一個線程也可以擁有多個協程

異步編程工具:協程、Futrue、Lamda。
分佈式系統中編程,涉及非常多的網絡通信,不可避免需要藉助於回調編程,不利於代碼閱讀。爲了解決回調函數這種對於代碼可讀性的破壞作用,提出很多改進方法,包括協程。協程延續同步編程習慣,但不同於多線程的是,協程並不會同時運行,它只是在需要阻塞的地方,用Yield()切換出去執行其他協程,然後當阻塞結束後,用Resume()回到剛剛切換的位置繼續往下執行。這相當於可以把回調函數的內容,接到Yield()調用的後面。
在這裏插入圖片描述
缺點:
Resume()的代碼還是需要在所謂“主線程”中運行。用戶必須自己從阻塞恢復的時候,去調用Resume()。
需要做棧保存,在切換到其他協程之後,棧上的臨時變量,也都需要額外佔用空間,限制協程代碼的寫法,讓開發者不能用太大的臨時變量。

協程:可以用類似同步的方法來寫異步程序,而無需把代碼塞到不同的回調函數裏面。yield關鍵字所在的代碼行,類似return,但是又代表着後續某個時刻,程序會從yield的地方繼續往下執行。這樣就把那些需要回調的代碼,從函數中得以解放出來,放到yield的後面。在很多客戶端遊戲引擎中,寫的代碼都是由一個框架,以每秒30幀的速度在反覆執行,爲了讓一些任務,可以分別放在各幀中運行,而不是一直阻塞導致“卡幀”,使用協程就是最自然和方便的——Unity3D就自帶協程的支持。

協程不是被操作系統內核所管理,而完全是由程序所控制(也就是在用戶態執行)。
這樣帶來的好處就是性能得到很大的提升,不會像線程切換那樣消耗資源。

語言支持

  • Lua:Lua從5.0版本開始使用協程,通過擴展庫coroutine來實現。
  • Python:通過 yield/send 的方式實現協程。在python 3.5以後, async/await 成爲更好的替代方案。實例:
def consume():
    while True:
        # consumer協程等待接收數據
        number = yield
        print('消費', num)


consumer = consume()
# 讓初始化狀態的consumer 協程先執行起來,在yield處停止
next(consumer)
for num in range(0, 100):
    print('生產', num)
    # 發送數據給consumer協程
    consumer.send(num)

創建一個叫做consumer的協程,並且在主線程中生產數據,協程中消費數據。
yield是python語法。當協程執行到yield關鍵字時,會暫停在那一行,等到主線程調用send方法發送數據,協程纔會接到數據繼續執行。
但是,yield讓協程暫停,和線程的阻塞是有本質區別的。協程的暫停完全由程序控制,線程的阻塞狀態是由操作系統內核來進行切換。
因此,協程的開銷遠遠小於線程的開銷。

  • Go:Go對協程的實現非常強大而簡潔,可以輕鬆創建成百上千個協程併發執行。
  • Java:Java的原生語法中並沒有實現協程,某些開源框架模擬出協程的功能,如:https://github.com/kilim/kilim

待學習

併發之痛 Thread,Goroutine,Actor

參考

死磕 java線程系列之線程模型
編碼複雜度和通信

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