# iOS 一窺併發程式設計底層(一)

語歌發表於2019-02-25

語歌部落格

邏輯控制流

在我們系統中通常是會有其它程式在執行,程式是可以告訴每一個程式它是獨自在使用處理器。這個時候如果有偵錯程式單步去執行程式,就會出現一系列的程式計數器( PC ) 值,這些值唯一的對應於包含在程式的可執行目標檔案的指令。這個所謂的 PC 值叫做 邏輯控制流

一句話簡單的介紹什麼是併發:

  • 如果邏輯控制流在時間上重疊就是併發 (concurrent)

e.g:

  • 往巨集觀上講:在計算機系統中硬體異常處理程式, 我們進行 Command + C的時候
  • 往微觀上講:I/O 多路複用,應用程式在一個程式的上下文中顯示地排程它們的邏輯流。邏輯流被模型化為狀態機,資料到達檔案描述符後,主程式顯式地從一個狀態轉換到另一個狀態。
  • .... 如果在底層上面扣太多,就不是 iOS 方面的內容了。

我們知道對 應用層的開發 都是通過對底層的一個 API 的封裝,這裡也會簡單介紹一下底層方面的理論。

如果要了解併發程式設計就免不了 程式,執行緒 這些字眼。


程式

我相信從事 IT 行業的開發人員,對於它是不陌生的,千篇一律的話我就不多說了。下面就簡單聊點不常知道的。
首先程式有獨立的虛擬地址空間,如果想要和其他流進行通訊,就是程式與程式之間進行通訊,控制流必須使用某種顯示的程式間通訊機制 (IPC)

執行緒

執行緒是執行在一個單一程式上下文的邏輯流,由核心進行排程。

Obj中國 上我看到有用 pthread 進行示例證明。 我這裡會適當的補充一點。

Posix 執行緒(Pthread) 是在 C 程式中處理執行緒的一個標準介面,在所有的 Linux 上都適用。 那麼 Objective- C 對它的依賴就可想而知了。

Pthread 定義了大約 60多個 個函式,它們分別用來進行建立,殺死,和回收執行緒.對執行緒安全地共享資料,通知對等執行緒系統狀態的變化等等。

直接適用 Pthread 的函式是非常繁瑣的,那麼到了 OC 這裡就免不了對它進行了二次封裝, 就這樣來到了 Cocoa 那麼到了 Swift 裡面也基本是換湯不換藥的拿來即用。


現在正式來到多執行緒的世界

先說點不厭其煩的廢話知識:

在 Objective-C 中常用的加鎖方式有用 @synchronized 來修飾變數,以此來保證變數在作用範圍內不會被其他執行緒改變。

那在 Swift 中是用 objc_sync_enterobjc_sync_exit 配合來使用加鎖

上面提到鎖相關的一些資訊,那麼它存在的目的無非就是一個:就是在多執行緒共享相同的程式變數

那麼它在底層的原理是什麼?正是這裡要講的。

問題
1.多執行緒存在的時候其基礎的記憶體模型是什麼?
2.變數如何對映到記憶體裡面去?
3.引用這些變數的執行緒有多少?

每個執行緒都有它自己獨立的執行緒上下文,包括執行緒的 ID, 棧 , 棧指標 ,程式計數器, 條件碼, 通用目的暫存器

每個執行緒和其他執行緒一起共享程式的上下文的剩餘部分。這包括整個使用者虛擬地址空間,它是由只讀文字,讀/寫資料, 棧以及所有的共享庫程式碼和資料區域組成。

任何執行緒都可以訪問共享虛擬記憶體的任意位置, 如果眾多執行緒中的某一個執行緒修改了一個記憶體位置,那其他的執行緒都能在它讀到這個位置時發現這個變化。

虛擬記憶體對相關變數的一些操作

  1. 全域性變數:
    虛擬記憶體的讀/寫區域只包含每個全域性變數的一個例項,任何執行緒都可以呼叫

  2. 本地變數:
    每個執行緒的棧都包含它自己的所有本地自動變數

  3. 本地的靜態變數:本地帶 Static 屬性, 虛擬記憶體的讀/寫區域只包含程式中宣告的每個本地靜態變數的一個例項

訊號量

計數器 (引用計數)
同步錯誤 synchronization error
進度圖 progress graph


計數器 (引用計數) 與 同步錯誤 (synchronization error)

在多執行緒訪問同一個全域性變數的時候,我們檢視對計數相關的彙編程式碼,過程大致如下:

  1. 載入全域性變數 cnt 到累加暫存器 %rdx (當前執行緒的暫存器 %rdx 的值)
  2. 增加 %rdx 的指令
  3. %rdx的更新值存回到共享變數 cnt 的指令

當然在iOS當中不會直接這樣計數,因為這樣會存在一個很大的問題:

當兩個執行緒同時對一個計數器的值進行讀取,並加1,再將結果寫到記憶體中去,這個時候計數器就會出現問題。因為計數器加了兩次而寫到記憶體中確是相當於只加了一次的那個值

引用 Obj中國 上面一個例子:

執行緒 A 和 B 都從記憶體中讀取出了計數器的值,假設為 1 ,然後執行緒A將計數器的值加1,並將結果 2 寫回到記憶體中。同時,執行緒B也將計數器的值加 1 ,並將結果 2 寫回到記憶體中。實際上,此時計數器的值已經被破壞掉了,因為計數器的值 1 被加 1 了兩次,而它的值卻是 2。

在iOS 開發的應用層上面來看就是加鎖等等一系列的操作。在真正核心底層方面好多文章是沒有具體去講的,您可以綜合性的看看其他的文章,推薦 Obj中國 上面關於多執行緒系統的講法。好了,我們繼續:

進度圖 progress graph

將 n 個併發執行緒的執行抽象為一條 n 維 笛卡爾空間 中的軌跡線.

每條軸的 k 對應執行緒 k 的進度。每個點代表執行緒 k已經完成了指令 I_k的狀態。

我們上面講的:

在多執行緒訪問同一個全域性變數的時候,我們檢視對計數相關的彙編程式碼,過程大致分為3步複製程式碼

我們這裡設定在 A 執行緒的時候步驟為:

第一步 第二步 第三步
A1 A2 A3

同理設定在 B 執行緒的時候步驟為:

第一步 第二步 第三步
B1 B2 B3

這個時候我們來看下圖

我們看到圖中有一個點 (A1,B3).
這個點的意思就是:當執行緒 A 完了第 A1 狀態的同時,執行緒 B 完成了 B3 狀態。

使用進度圖的目的就是講指令執行模型轉化為從一種狀態到另一種狀態的轉換。

這樣就可以把程式的執行歷史轉換為狀態空間中的一條軌跡線。

對於執行緒不管是 A 或者 B 也好,對全域性變數的的操作(A1,A2,A3)步驟或者 (B1,B2,B3)步驟的過程中構成了一個臨界區,這個臨界區不應該和其他程式的臨界區交替執行。我們確保每個執行緒在執行它的臨界區中的指令時,擁有對共享變數 的 互斥 的訪問( Mutually exclusive access). 通常這種現象稱為互斥(Mutual exclusion).

這樣在上圖裡面會出現這樣的規則:相同指令不能再同一時刻完成,對角線的線是不存在的。

兩個臨界區的交集形成的狀態空間區域稱為不安全區(unsafe region)

安全軌跡線:不在不安全區的軌跡線
不安全軌跡線:雷區的軌跡線

任何安全軌跡線都將正確地更新共享計數器。為了保證任意的全域性變數在併發執行緒的正確執行,我們就必須以某種方式同步執行緒,使他們總是有一條安全軌跡線。其思想原理的基本思想就是基於 訊號量

訊號量: (semaphore) 一種特殊型別的變數。
訊號量以s表示.是具有非負整數值的全域性變數,只能有兩種特殊的操作來處理,這兩種操作稱為 P 和 V:

P(s): 如果 s 是非零的,那麼P 將 s 減1,並且立即返回。如果 S 為零,那麼就掛起這個執行緒,直到 s 為零,而一個 V 操作會重啟這個執行緒。在重啟之後,P 操作將 s減1,並將控制返回給呼叫者。

V(s): V操作將 s 加1。如果有任何執行緒阻塞在 P 操作等待 s 變成非零,那麼 V 操作會重啟這些執行緒中的一個,然後該執行緒將 s 減1,完成它的 P 操作。

P 中的測試和減1操作是不可分割的,一旦預測訊號量s 變為非零,就會將s減1,不能有中斷操作,這個過程中不會有中斷。 V 的加1 操作也是不可分割的。

沒有中斷的操作

載入 加1 儲存訊號

ps: V 的定義中沒有定義等待執行緒被重啟動的順序。唯一的要求是 V 必須只能重啟一個正在等待的執行緒。因此,當有多個執行緒在等待同一個訊號量時,就不能預測 V 操作要重啟哪一個執行緒。

P和V 的定義確保了一個正在執行的程式絕不可能進入這一種狀態,也就是一個正確初始化了的訊號量有一個負值。這個屬性稱為 訊號量不變性(semaphore invariant)

使用訊號量來實現互斥

作用是:
將每個全域性變數 與 一個訊號量 s = 1 聯絡起來,然後用 P(s) 和 V(s) 操作將相應的臨界區包圍起來。這種方式成為 二元訊號量 (binary semaphore),它的值要麼是 0 要麼是 1。以提供互斥為目的的二元型號量常常稱為 互斥鎖 (mutex).

那麼在一個互斥鎖上執行 P 操作稱為對互斥鎖加鎖。執行 V操作稱為對互斥鎖解鎖。對一個互斥鎖加了鎖但是還沒有解鎖的執行緒稱為佔用了這個互斥鎖。 一個被用作一組可用資源的計數器的訊號量被稱為 計數訊號量

如上面的雷區圖,在雷區內因為訊號量的不確定性故: s < 0

以上看到的仍然是坑: 因為上面是單處理器的講解
但是有一個是萬用的:同步對共享變數的訪問是必須的。

多執行緒中對相同資源的訪問:
案例1:
在多媒體開發過程中對視訊的幀編碼,並實時播放。這個時候就會有一個快取的東西存在,其存在的目的是為了減少視訊流的抖動,引起的原因是幀的編碼與解碼時與資料相關的差異引起的。

案例2:
我們開發過程中對手機螢幕點選事件的產生後,該事件先進入快取中,然後多執行緒根據優先順序來從緩衝區裡面取出該事件進行響應。這就能很好解釋有時點選螢幕卡屏了一會兒才響應。

飢餓問題:
這個網上帖子氾濫: 傳送門 Obj中國

多個執行緒並行處理分配給它們的區域處理方法:
主執行緒給其他開的執行緒一個整數理解為該執行緒的 ID。每個執行緒用它的ID來決定它應該計算序列的哪一部分。

##並行程式的效能

執行時間是衡量程式效能的最終標準。相對衡量標準能夠說明並行程式有多好地利用了潛在的並行性。
並行程式的加速比(speedup)通常定義為: Sp = T1/Tp

p 是處理器的核樹,Tk 是在 K 個核上的執行時間。這個公式被稱為:強擴充套件(strong scaling).

  1. 當T1是程式順序執行版本的執行時間時,Sp稱為 絕對加速比 (absolute speedup).
  2. 當T1是程式並行版本在一個核上的執行時間,Sp稱為 相對加速比 (absolute speedup).

絕對加速比會比相對加速比更加難以測量,因為測量絕對加速比需要程式的兩種不同的版本。對於複雜的並行程式碼,創造一個獨立的順序版本也不現實。

效率: Ep = Sp/p = T1/pTp

弱擴充套件:(weak scaling): 在增加處理器數量的同時,增加問題的規模,這樣隨著處理器的數量的增加,每個處理器執行的工作量儲存不變,在這樣的情況下加速比和效率被表達為單位時間完成的工作量。

##執行緒安全
首先被稱為執行緒安全是當且僅當被多個多執行緒反覆的呼叫,它才會一直產生正確的結果。如果一個函式設計的不是執行緒安全的,它就是執行緒不安全的。

執行緒不安全的函式定義:

  1. 對全域性變數的保護
  2. 儲存跨越多個呼叫的狀態函式。如:隨機數生產的函式
  3. 返回指向靜態變數的指標的函式。
  4. 呼叫執行緒不安全函式的函式。

每個的具體例子有點多分下一章節進行

iOS 一窺併發程式設計底層(二)

相關文章