iOS概念攻堅之路(四):多線程

前言

咱們如今所使用的操做系統模式是 多任務(Multi-tasking)系統,操做系統接管全部的硬件資源,並且自己運行在一個受硬件保護的級別。全部的應用程序都是以 進程(Progress) 的方式運行在比操做系統權限更低的級別。每一個進程都有本身獨立的地址空間,使得進程之間的地址空間相互隔離。CPU 由操做系通通一進行分配,每一個進程根據進程優先級的高低都有機會獲得 CPU,可是,若是運行時間超過了必定的事件,操做系統會暫停該進程,將 CPU 資源分配給其餘等待運行的進程。這種 CPU 的分配方式即所謂的 搶佔式(Preemptive),操做系統能夠強制剝奪 CPU 資源而且分配給它認爲目前最須要的進程,若是操做系統分配給每一個進程的時間都很短,即 CPU 在多個進程間快速的切換,從而形成了不少進程在同時運行的假象。目前幾乎全部現代的操做系統都是採用這種方式。html

計算機發展早期,一個 CPU 只能運行一個程序,當執行 I/O 操做,好比讀取磁盤數據時,CPU 就會處於空閒狀態,這顯然是一種浪費。後來人們迅速發明了 多道程序(Multiprogramming),當某個程序無需使用 CPU 時,監控程序就把另外的正在等待 CPU 資源的程序啓動,不過它沒有一個優先級的概念,就算某些任務急需 CPU,也極可能須要等待很長的時間。通過改進後,人們又發明了 分時系統(Time-Share System),每一個程序運行一段時間後都主動讓出 CPU 給其餘程序,使得一段時間內每一個程序都有機會運行一小段時間。不過這種系統的問題在於,若是一個程序在進行一個很耗時的操做,一直霸佔 CPU,那麼操做系統也是沒有辦法的,好比一個程序進入了一個 while(1) 的死循環,那麼整個系統都會中止。再進一步的發展,就是咱們上面提到的多任務系統了。ios

如此發展的目的是,儘量最大限度的利用 CPU,到這裏,出現了一個進程的概念,而線程與進程,有着脫不開的關係。程序員

什麼是進程

維基百科:算法

進程(Process),是計算機中 已運行程序 的實體。進程曾經是分時系統的基本運做單位。在面向進程設計的系統(如早期的 UNIX,Linux2.4 及更早的版本)中,進程是程序的基本執行實體;在面向線程設計的系統(如當代多數操做系統,Linux 2.6 及更新版本)中,進程自己不是基本運行單位,而是 線程的容器。程序自己只是 指令,數據及其組織形式的描述,進程纔是程序(那些指令和數據)的真正運行實現。安全

用戶下達運行程序的命令後,就會產生進程。同一程序可產生多個進程(一對多關係),以容許同時有多位用戶運行同一程序,卻不會相互衝突。網絡

進程須要一些資源才能完成工做,如 CPU 使用時間、存儲器、文件及 I/O 設備,且爲依序逐一進行,也就是每一個 CPU 核心任什麼時候間內僅能運行一項進程。多線程

百度百科:併發

進程(Process) 是計算機中的 程序關於某數據集合上的一次運行活動,是系統進行 資源分配 和調度的基本單位,是操做系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。異步

進程是一個具備獨立功能的程序關於某數據集合的一次運行活動。它能夠申請和擁有系統資源,是一個 動態 的概念,是一個活動的實體。它不僅是程序的代碼,還包括當前的活動,經過程序計數器的值和處理寄存器的內容來表示。函數

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

因此,進程是程序的一個實體,是執行中的程序,而程序是指令、數據及其組織形式的描述。程序不能單獨執行,只有將程序加載到內存中,系統爲它分配資源後纔可以執行,這種 執行的程序 稱之爲進程。因此進程是一個動態的概念,與程序的區別在於,程序是指令的集合,是進程運行的靜態描述文本,而進程則是程序在系統上執行的動態活動。

能夠這麼理解,咱們寫的 APP,是一個程序,咱們裝到手機上,此時它還不是進程,當咱們點擊它,系統爲它分配資源,運行,此時它能夠被稱爲進程。

另外二者都提到,在現代的面向程序設計的計算機結構中,進程是線程的容器。在現在的操做系統中,線程纔是最小的調度單位,而進程,是資源分配的最小單位,一個進程包含一個或多個線程。

來看看進程的內容:

  • 那個程序的可執行機器代碼的一個在存儲器的映象。
  • 分配到別的存儲器(一般是一個虛擬的一個存儲器區域)。存儲器的內容包括可執行代碼、特定於進程的數據(輸入、輸出)、調用堆棧、堆棧(用於保存運行時運輸中途產生的數據)。
  • 分配給該進程的資源和操做系統描述符,諸如文件描述符(UNIX術語)或文件句柄(Windows)、數據源和數據終端。
  • 安全特性,諸如進程擁有者和進程的權限集(能夠允許的操做)。
  • 處理器狀態(中文),諸如寄存器內容、物理存儲器定址等。當進程正在運行時,狀態一般存儲在寄存器,其餘狀況在存儲器。

什麼是線程

內核線程、輕量級進程、用戶線程

前面提到,進程是線程的容器,而在現代的多數操做系統中,線程是調度的最小單位。咱們來看看線程的定義:

維基百科:

線程(Thread) 是操做系統可以進行運算調度的最小單位。它被包含在進程之中,是進程中的 實際運做單位。一條線程指的是 進程中一個單一順序的控制流,一個進程中能夠併發多個線程,每條線程並行執行不一樣的任務。在 Unix System V 及 SunOS 中也被稱爲輕量級進程(lightweight processes),但輕量進程更多指內核線程(kernel thread),而把用戶線程(user thread)稱爲線程。

這邊提到了幾個概念:內核線程,輕量級進程,用戶線程,咱們分別來看看它們的具體概念:

內核線程

內核線程就是內核的分身,一個分身能夠處理一件特定事情。這在處理異步事件如異步 IO 時特別有用。內核線程的使用是廉價的,惟一使用的資源就是內核棧和上下文切換時保存寄存器的空間。支持多線程的內核叫作多線程內核(Multi-Threads kernel)。

內核線程只運行在內核態,不受用戶態上下文的拖累。

輕量級進程

輕量級進程(LWP)是一種由 內核支持的用戶線程。它是基於內核線程的高度抽象,所以只有先支持內核線程,纔能有 LWP。每個進程有一個或多個 LWP,每一個 LWP 由一個內核線程支持。這種模型被稱爲一對一模型。在這種實現的操做系統中,LWP 就是用戶線程。

因爲每一個 LWP 都與一個特定的內核線程相關聯,所以每一個 LWP 都是一個獨立的線程調度單元。即便有一個 LWP 在系統調用中阻塞,也不會影響整個進程的執行。

輕量級進程的侷限性:

  • 大多數 LWP 的操做,如創建、析構以及同步,都須要進行系統調用,系統調用的代價相對較高(須要在用戶態和內核態中切換)
  • 每一個 LWP 都須要有一個內核線程支持,所以 LWP 要消耗內核資源(內核線程的棧空間)。所以一個系統不能支持大量的 LWP。(圖片的 P 指進程)

將之稱之爲輕量級進程的緣由多是:在內核線程的支持下,LWP 是獨立的調度單元,就像普通的進程同樣。因此 LWP 的最大特色仍是每一個 LWP 都有一個內核線程支持。

用戶線程

LWP 雖然本質上屬於用戶線程,但 LWP 線程庫是創建在內核之上的,LWP 的許多操做都要進行系統調用,所以效率不高。而這裏的用戶線程指的是 徹底創建在用戶空間的線程庫,用戶線程的創建、同步、銷燬、調度徹底在用戶空間完成,不須要內核的幫助,所以這種線程的操做是及其快速且低消耗的。

上圖是最初的一個用戶線程模型,從中能夠看出,進程中包含線程,用戶線程在用戶空間中實現,內核並無直接對用戶線程進行調度,內核的調度對象和傳統進程同樣,仍是進程自己,內核並不知道用戶線程的存在。用戶線程之間的調度由在用戶控件的線程庫實現。

這是多對一模型,其缺點在於一個用戶線程若是阻塞在系統調用中,則整個進程都將會阻塞。

增強版的用戶線程 —— 用戶線程+LWP

這種模型是所謂的多對多模型。用戶線程庫仍是徹底創建在用戶空間中,所以用戶線程的操做仍是很廉價,所以能夠創建任意多須要的用戶線程。操做系統提供了 LWP 做爲用戶線程和內核線程之間的橋樑。LWP 仍是和前面提到的同樣,具備內核線程支持,是內核的調度單元,而且用戶線程的系統調用要經過 LWP,所以進程中某個用戶線程的阻塞不會影響整個進程的執行。用戶線程庫將創建的用戶線程關聯到 LWP 上,LWP 與用戶線程的數量不必定一致。當內核調度到某個 LWP 上時,此時與該 LWP 關聯的用戶線程就被執行。

不少文獻中認爲輕量級進程就是線程,實際上這種說法並不徹底正確,只有在用戶線程徹底由輕量級進程構成時,才能夠說輕量級進程就是線程。

更多有關內核線程、輕量級進程、用戶線程三種線程的概念,能夠看看 這篇文章

在現代操做系統中,再也不將進程當作操做的基本單元了,而是把線程當作基本單元,一個進程中能夠存在多個線程。一個進程內的全部線程都共享虛擬內存空間。進程這個概念依然以一個或多個線程的 容器 的形式保存下來。進程每每都是多線程的,當一個進程只是單線程時,進程和線程兩個屬於能夠互換使用。

線程的結構、訪問權限

一個標準的線程由線程 ID、當前指令指針(PC)、寄存器集合和堆棧組成。一般意義上,一個進程由一個到多個線程組成,各個線程之間共享程序的內存空間(包括代碼段、數據段、堆等)及一些進程級的資源(如打開文件和信號),下面是線程和進程的一個關係結構圖:

線程能夠訪問進程內存裏的全部數據,甚至包括其餘線程的堆棧(若是它知道其餘線程的堆棧地址,不過這種狀況不多見),在實際運用中,線程也擁有本身的私有存儲空間,包括如下方面:

  • 棧(儘管並不是徹底沒法被其餘線程訪問,但通常狀況下仍然能夠認爲是私有的資源)
  • TLS(Thread Local Storage,線程局部存儲)。TLS 是某些操做系統爲線程單獨提供的私有空間,一般只具備頗有限的容量
  • 寄存器(包括 PC 寄存器),寄存器是執行流的基本數據,所以爲線程私有

線程與進程的數據是否私有以下表:

線程私有 進程之間共享(進程全部)
局部變量 全局變量
函數的參數 堆上的數據
TLS 函數的靜態變量
程序代碼,任何線程都有權利讀取並執行任何代碼
打開的文件,A 線程打開的文件能夠由 B 線程讀寫

線程調度與優先級

無論是多處理器仍是單處理器,咱們看到的線程彷佛老是 「併發」 執行的,實際狀況是,只有當線程數量小於或等於處理器的數量時(而且操做系統支持多處理器),線程的併發纔是真正的併發(也就是並行),不一樣的線程運行在不一樣的處理器上,彼此之間互不相干。對於線程數量大於處理器數量的狀況,至少有一個處理器會運行多個線程,此時的併發只是一種模擬出來的狀態:操做系統會讓這些多線程程序輪流執行,每次僅執行以小段時間(一般是幾十到幾百毫秒),這樣每一個線程 「看起來」 在同時執行。

這樣的一個不斷在處理器上切換不一樣線程的行爲稱之爲 線程調度(Thread Schedule),在線程調度中,線程一般擁有至少三種狀態,分別是:

  • 運行(Running):此時線程正在執行
  • 就緒(Ready):此時線程能夠馬上執行,但 CPU 已經被佔用
  • 等待(Waiting):此時線程正在等待某一事件(一般是 I/O 或同步)發生,沒法執行

來看一下線程的生命週期:

處於運行中線程擁有一段能夠執行的時間,這段時間稱爲時間片(Time Slice),當時間片用盡的時候,該線程進入就緒狀態。若是在時間片用盡以前線程就開始等待某事件,那麼它將進入等待狀態。每當一個線程離開運行狀態時,調度系統就會選擇一個其餘的就緒線程繼續執行。在一個處於等待狀態的線程所等待的事件發生以後,該線程將進入就緒狀態。

線程調度自多任務操做系統問世以來就不斷被提出不一樣的方案和算法。如今主流的調度方式儘管各不相同,但都帶有 優先級調度(Priority Schedule)輪轉法(Round Robin) 的痕跡。所謂輪轉法,便是以前提到的讓各個線程輪流執行一小段時間的方法,這決定了線程之間是交錯運行的。而優先級調度則決定了線程按照什麼順序輪流執行。在具備優先級調度的系統中,線程都擁有各自的 線程優先級(Thread Priority)。具備高優先級的線程會更早的執行,而低優先級的線程經常要等到系統中已經沒有高優先級的可執行的線程存在時纔可以執行。

系統會根據不一樣線程的表現自動調整優先級,以使得調度更有效率。例如一般狀況下,頻繁的進入等待狀態(進入等待狀態,會放棄以後仍然課佔用的時間份額,也就是咱們說的線程休眠,不會佔用 CPU 資源)的線程(例如處理 I/O 的線程)比頻繁進行大量計算,以致於每次都要把時間片所有用盡的線程要受歡迎的多。通常把頻繁等待的線程稱之爲 IO 密集型線程(IO Bound Thread),而把不多等待的線程稱爲 CPU 密集型線程(CPU Bound Thread)。IO 密集型線程老是比 CPU 密集型線程容易獲得優先級的提高。

在優先級調度下,存在一種 餓死(Starvation) 的現象,一個線程被餓死,是說它的優先級較低,在它執行以前,老是有高優先級的線程要執行,所以這個低優先級線程始終沒法執行。當一個 CPU 密集型線程得到較高的優先級時,許多低優先級的線程就極可能餓死。而一個高優先級的 IO 密集型線程因爲大部分時間都處於等待狀態,所以相對不容易形成其餘線程餓死。爲了不餓死現象,調度系統經常會逐步提高那些等待了過長時間的得不到執行的線程的優先級。在這樣的手段下,一個線程只要等待足夠多的事件,其優先級必定會提升到足夠讓它執行的程度。

總結一下,在優先級調度的環境下,線程的優先級改變通常有三種方式:

  • 用戶指定優先級
  • 根據進入等待狀態的頻繁程度提高或下降優先級
  • 長時間得不到執行而被提高優先級

線程安全

多線程程序處於一個多變的環境當中,可訪問的全局變量和堆數據隨時均可能被其餘的線程改變,所以多線程程序在併發時數據的一致性變得很是重要。

爲了不多個線程同時讀寫同一個數據而產生不可預料的後果,咱們要將各個線程對同一個數據訪問 同步(Synchronization)。所謂同步,即指在一個線程訪問數據未結束的時候,其餘線程不得對同一個數據進行訪問。如此,對數據的訪問被原子化了。

原子操做:單指令的操做,不管如何,單條指令的執行都不會被打斷。

同步的最多見方法是使用 鎖(Lock)。鎖是一種非強制機制,每個線程在訪問數據或資源以前首先試圖 獲取(Acquire) 鎖,並在訪問結束以後 釋放(Release) 鎖。在鎖已經被佔用的時候試圖獲取鎖時,線程會等待,直到鎖從新可用。

二元信號量(Binary Semaphore) 是最簡單的鎖,它只有兩種狀態:佔用與非佔用。它適合只能被惟一一個線程獨佔訪問的資源。當二元信號量處於非佔用狀態時,第一個試圖獲取該二元信號量的線程會得到該鎖,並將二元信號量置爲佔用狀態,此後其餘的全部試圖獲取該二元信號量的線程將會等待,直到該鎖被釋放。

對於容許多個線程併發訪問的資源,多元信號量簡稱 信號量(Semaphore),它是一個很好的選擇。一個初始值爲 N 的信號量容許 N 個線程併發訪問。線程訪問資源的時候首先獲取信號量,進行以下操做:

  1. 將信號量的值減 1
  2. 若是信號量的值小於 0,則進入等待狀態,不然繼續執行

訪問完資源以後,線程釋放信號量,進行以下操做:

  1. 將信號量的值加 1
  2. 若是信號量的值小於 1,喚醒一個等待中的線程

互斥量(Mutex) 和二元信號量很相似,資源僅同時容許一個線程訪問,但和信號量不一樣的是,信號量在整個系統能夠被任意線程獲取並釋放,也就是說,同一個信號量能夠被系統中的一個線程獲取以後由另外一個線程釋放。而互斥量則要求哪一個線程獲取了互斥量,哪一個線程就要負責釋放這個鎖,其餘線程去釋放互斥量是無效的。

臨界區(Cirtical Section) 是比互斥量更加嚴格的同步手段。在術語中,把臨界區的鎖的獲取稱爲進入臨界區,而把鎖的釋放稱爲離開臨界區。臨界區和互斥量與信號量的區別在於,互斥量和信號量在系統的任何進程裏都是可見的,也就是說,一個進程建立了一個互斥量或信號量,另外一個進程試圖去獲取該鎖是合法的。然而,臨界區的做用範圍僅限於本進程,其餘的進程沒法獲取該鎖。除此以外。臨界區具備和互斥量相同的性質。

讀寫鎖(Read-Write Lock) 致力於一種更加特定的場合的同步。對於一段數據,多個線程同時讀取老是沒有問題的,但假設操做都不是原子型,只要有任何一個線程試圖對這個數據進行修改,就必須使用同步的手段來避免出錯。若是咱們使用上述信號量、互斥量或臨界區中的任何一種來進行同步,儘管能夠保證程序正確,但對於讀取頻繁,而僅僅偶爾寫入的狀況,會顯得很是低效。讀寫鎖能夠避免這個問題。對於同一個鎖,讀寫鎖有兩種獲取方式,共享的(Shared)獨佔的(Exclusive)。當鎖處於自由的狀態時,試圖以任何一種方式獲取鎖都能成功,並將鎖置於對應的狀態。若是鎖處於共享狀態,其餘線程以共享的方式獲取鎖仍然會成功,此時這個鎖被分配給了多個線程。然而,若是其餘線程試圖以獨佔的方式獲取已經處於共享狀態的鎖,那麼它將必須等待鎖被全部的線程釋放。相應的,處於獨佔狀態的鎖將阻止任何其餘線程獲取該鎖,不論它們試圖以哪一種方式獲取。讀寫鎖的行爲能夠總結爲下表:

讀寫鎖狀態 以共享方式獲取 以獨佔方式獲取
自由 成功 成功
共享 成功 等待
獨佔 等待 等待

條件變量(Condition Variable) 做爲一種同步手段,做用相似於一個柵欄。對於條件變量,線程能夠有兩種操做,首先線程能夠等待條件變量,一個條件變量能夠被多個線程等待。其次,線程能夠喚醒條件變量,此時某個或全部等待此條件變量的線程都會被喚醒並繼續支持。也就是說,使用條件變量可讓許多線程一塊兒等待某個事件的發生,當事件發生時(條件變量被喚醒),全部的線程能夠一塊兒恢復執行。

鎖與線程同步,會單獨再開一篇文章來講,鎖的概念都是同樣的,只是實現的手段不同。

線程和進程的由來

我在看 進程和線程的一個簡單解釋 這篇文章的時候,看到一個回答,用來講明進程和線程的由來比較合適:

  1. 在單核計算機裏,有一個資源是沒法被多個應用程序並行使用的:CPU。

沒有操做系統的狀況下,一個程序一直獨佔着所有 CPU。

若是要有兩個任務來共享一個 CPU,程序員就須要仔細地爲程序安排好運行計劃 —— 某時刻 CPU 由程序 A 來獨享,下一時刻 CPU 由程序 B 來獨享。

而這種安排計劃後來成爲 OS 的核心組件,被單獨命名爲 Scheduler(調度器)。它關心的只是怎樣把單個 CPU 的運行拆分紅一段一段的 「運行片」,輪流分給不一樣的程序去使用,而在宏觀上,由於分配切換的速度極快,就製造出多線程並行在一個 CPU 上的假象。

  1. 在單核計算機裏,有一個資源能夠被多個程序共用,然而會引出麻煩:內存。

在一個只有調度器,沒有內存管理組件的操做系統上,程序員須要手工爲每一個程序安排運行的空間 —— 程序 A 使用武力地址 0x00-0xff,程序 B 使用物理地址 0x100-0x1ff,等等。

然而這樣作有個很大的問題:每一個程序都要協調商量好怎樣使用同一個內存上的不一樣空間,軟件系統和硬件系統千差萬別,使這種定製方案沒有可行性。

爲了解決這個麻煩,計算機引入了「虛擬內存」的概念,從三方面入手來作:

  • 硬件上,CPU 增長了一個專門的模塊叫 MMU,負責轉換虛擬地址和物理地址
  • 操做系統上,操做系統增長了另外一個核心組件:「Memory Management」,即內存管理模塊,它管理物理內存、虛擬內存相關的一系列事務。
  • 應用程序上,發明了一個叫作「進程」的模型,每一個進程都用 徹底同樣 的虛擬地址空間,而後經由操做系統和硬件 MMU 協做,映射到不一樣的物理地址空間上。不一樣的「進程」都有各自獨立的物理內存空間,不用一些特殊手段,是沒法訪問別的進程的物理內存的。
  1. 如今,不一樣的應用程序,能夠不關心底層的物理內存分配,也不關心 CPU 的協調共享了。然而還有一個問題存在:有一些程序,想要共享 CPU,而且還要共享一樣的物理內存,這時候,一個叫「線程」的模型就出現了,它們被包裹在進程裏面,在調度器的管理下共享 CPU,擁有一樣的虛擬空間地址,同時也共享同一個物理地址空間,然而,它們沒法越過包裹本身的進程,去訪問另一個進程的物理地址空間。

爲何要使用多線程

  • 某個操做可能會陷入長時間等待,等待的線程會進入睡眠狀態,沒法繼續執行。多線程執行能夠有效利用等待的時間,典型的例子就是等待網絡響應。
  • 某個操做(經常是計算)會消耗大量的時間,若是隻有一個線程,程序和用戶之間的交互會中斷。多線程可讓一個線程負責交互,另外一個線程負責計算。
  • 程序邏輯自己就要求併發操做,例如一個多端下載軟件。
  • 多 CPU 或多核計算機,自己就具備同時執行多個線程的能力,所以單線程程序沒法全面的發揮計算機的所有計算能力。
  • 相對於多進程引用,多線程在數據共享方面效率要高不少。

總結

其實咱們最主要是兩個問題:什麼是進程和什麼是線程。

什麼是進程?

進程是計算機中已運行程序的主體,在以前的分時系統中,是系統的基本運做單位。不過在現在的面向線程設計的系統中,進程是線程的容器。它的概念主要有兩點:第一,進程是一個實體,每個進程都有它本身的地址空間。第二,進程是一個執行中的程序,程序是一個沒有生命的實體,只有處理器賦予程序生命(如點擊運行),它才能成爲一個活動的實體,也就是進程。

進程是系統進行資源分配的最小單位。

什麼是線程?

線程,有時候被稱爲輕量級進程,它包含在進程之中,是進程中的實際運做單位,一條線程指的是進程中一個單一順序的執行流。線程共享進程的全部數據而且擁有本身私有的存儲空間。線程又份內核線程和用戶線程。

線程是系統進行調度的最小單位。

當咱們要運行一個程序,系統爲咱們分配資源,而後運行,此時稱爲進程,然而真正運行的不是進程,而是進程內的某個執行流,也就是線程,一個進程至少有一條線程。

另外還有線程相關的一些知識點,好比線程的訪問權限、結構、生命週期、安全等。

關於線程和進程的話題經久不息,若是文中有理解錯誤的地方,歡迎你們指出。

參考文章

iOS 多線程全套

內核線程、輕量級進程、用戶線程三種線程概念解惑(線程≠輕量級進程)

進程和線程的一個簡單解釋

相關文章
相關標籤/搜索