邏輯控制流
在我們系統(tǒng)中通常是會(huì)有其它程序在運(yùn)行探橱,進(jìn)程是可以告訴每一個(gè)程序它是獨(dú)自在使用處理器。這個(gè)時(shí)候如果有調(diào)試器單步去執(zhí)行程序,就會(huì)出現(xiàn)一系列的程序計(jì)數(shù)器( PC ) 值镜廉,這些值唯一的對(duì)應(yīng)于包含在程序的可執(zhí)行目標(biāo)文件的指令。這個(gè)所謂的 PC 值叫做 邏輯控制流
一句話簡(jiǎn)單的介紹什么是并發(fā):
- 如果邏輯控制流在時(shí)間上重疊就是并發(fā) (
concurrent
)
e.g:
往宏觀上講:在計(jì)算機(jī)系統(tǒng)中硬件異常處理程序, 我們進(jìn)行 `Command + C `的時(shí)候
往微觀上講:I/O 多路復(fù)用愚战,應(yīng)用程序在一個(gè)進(jìn)程的上下文中顯示地調(diào)度它們的邏輯流娇唯。邏輯流被模型化為狀態(tài)機(jī),數(shù)據(jù)到達(dá)文件描述符后寂玲,主程序顯式地從一個(gè)狀態(tài)轉(zhuǎn)換到另一個(gè)狀態(tài)塔插。
- .... 如果在底層上面扣太多,就不是 iOS 方面的內(nèi)容了敢茁。
我們知道對(duì) 應(yīng)用層的開發(fā) 都是通過對(duì)底層的一個(gè) API 的封裝,這里也會(huì)簡(jiǎn)單介紹一下底層方面的理論。
如果要了解并發(fā)編程就免不了 進(jìn)程,線程 這些字眼留美。
進(jìn)程
我相信從事 IT
行業(yè)的開發(fā)人員彰檬,對(duì)于它是不陌生的伸刃,千篇一律的話我就不多說了。下面就簡(jiǎn)單聊點(diǎn)不常知道的逢倍。
首先進(jìn)程有獨(dú)立的虛擬地址空間捧颅,如果想要和其他流進(jìn)行通信,就是進(jìn)程與進(jìn)程之間進(jìn)行通信较雕,控制流必須使用某種顯示的進(jìn)程間通信機(jī)制 (IPC
)
線程
線程是運(yùn)行在一個(gè)單一進(jìn)程上下文的邏輯流,由內(nèi)核進(jìn)行調(diào)度碉哑。
在 Obj中國(guó) 上我看到有用 pthread
進(jìn)行示例證明。 我這里會(huì)適當(dāng)?shù)难a(bǔ)充一點(diǎn)亮蒋。
Posix 線程(Pthread) 是在 C 程序中處理線程的一個(gè)標(biāo)準(zhǔn)接口扣典,在所有的 Linux 上都適用。 那么 Objective- C 對(duì)它的依賴就可想而知了慎玖。
Pthread 定義了大約 60多個(gè) 個(gè)函數(shù)贮尖,它們分別用來進(jìn)行創(chuàng)建,殺死,和回收線程.對(duì)線程安全地共享數(shù)據(jù),通知對(duì)等線程系統(tǒng)狀態(tài)的變化等等趁怔。
直接適用 Pthread 的函數(shù)是非常繁瑣的,那么到了 OC 這里就免不了對(duì)它進(jìn)行了二次封裝, 就這樣來到了 Cocoa 那么到了 Swift 里面也基本是換湯不換藥的拿來即用湿硝。
現(xiàn)在正式來到多線程的世界
先說點(diǎn)不厭其煩的廢話知識(shí): 鎖
在 Objective-C 中常用的加鎖方式有用 @synchronized
來修飾變量,以此來保證變量在作用范圍內(nèi)不會(huì)被其他線程改變润努。
那在 Swift 中是用 objc_sync_enter
與 objc_sync_exit
配合來使用加鎖
上面提到鎖相關(guān)的一些信息关斜,那么它存在的目的無非就是一個(gè):就是在多線程共享相同的程序變量
那么它在底層的原理是什么?正是這里要講的铺浇。
問題 |
---|
1.多線程存在的時(shí)候其基礎(chǔ)的內(nèi)存模型是什么痢畜? |
2.變量如何映射到內(nèi)存里面去? |
3.引用這些變量的線程有多少随抠? |
每個(gè)線程都有它自己獨(dú)立的線程上下文,包括線程的 ID, 棧 , 棧指針 ,程序計(jì)數(shù)器, 條件碼, 通用目的寄存器
每個(gè)線程和其他線程一起共享進(jìn)程的上下文的剩余部分裁着。這包括整個(gè)用戶虛擬地址空間,它是由只讀文本,讀/寫數(shù)據(jù), 棧以及所有的共享庫代碼和數(shù)據(jù)區(qū)域組成拱她。
任何線程都可以訪問共享虛擬內(nèi)存的任意位置, 如果眾多線程中的某一個(gè)線程修改了一個(gè)內(nèi)存位置二驰,那其他的線程都能在它讀到這個(gè)位置時(shí)發(fā)現(xiàn)這個(gè)變化。
虛擬內(nèi)存對(duì)相關(guān)變量的一些操作
全局變量:
虛擬內(nèi)存的讀/寫區(qū)域只包含每個(gè)全局變量的一個(gè)實(shí)例秉沼,任何線程都可以調(diào)用
本地變量:
每個(gè)線程的棧都包含它自己的所有本地自動(dòng)變量
本地的靜態(tài)變量:
本地帶 Static 屬性, 虛擬內(nèi)存的讀/寫區(qū)域只包含程序中聲明的每個(gè)本地靜態(tài)變量的一個(gè)實(shí)例
信號(hào)量
計(jì)數(shù)器 (引用計(jì)數(shù))
同步錯(cuò)誤 synchronization error
進(jìn)度圖 progress graph
計(jì)數(shù)器 (引用計(jì)數(shù)) 與 同步錯(cuò)誤 (synchronization error)
在多線程訪問同一個(gè)全局變量的時(shí)候桶雀,我們查看對(duì)計(jì)數(shù)相關(guān)的匯編代碼,過程大致如下:
- 加載全局變量
cnt
到累加寄存器%rdx
(當(dāng)前線程的寄存器%rdx
的值) - 增加
%rdx
的指令 - 將
%rdx
的更新值存回到共享變量 cnt 的指令
當(dāng)然在iOS當(dāng)中不會(huì)直接這樣計(jì)數(shù)唬复,因?yàn)檫@樣會(huì)存在一個(gè)很大的問題:
當(dāng)兩個(gè)線程同時(shí)對(duì)一個(gè)計(jì)數(shù)器的值進(jìn)行讀取矗积,并加1,再將結(jié)果寫到內(nèi)存中去敞咧,這個(gè)時(shí)候計(jì)數(shù)器就會(huì)出現(xiàn)問題棘捣。因?yàn)橛?jì)數(shù)器加了兩次而寫到內(nèi)存中確是相當(dāng)于只加了一次的那個(gè)值
引用 Obj中國(guó) 上面一個(gè)例子:
線程 A 和 B 都從內(nèi)存中讀取出了計(jì)數(shù)器的值,假設(shè)為 1 休建,然后線程A將計(jì)數(shù)器的值加1乍恐,并將結(jié)果 2 寫回到內(nèi)存中评疗。同時(shí),線程B也將計(jì)數(shù)器的值加 1 茵烈,并將結(jié)果 2 寫回到內(nèi)存中百匆。實(shí)際上,此時(shí)計(jì)數(shù)器的值已經(jīng)被破壞掉了呜投,因?yàn)橛?jì)數(shù)器的值 1 被加 1 了兩次加匈,而它的值卻是 2。
在iOS 開發(fā)的應(yīng)用層上面來看就是加鎖等等一系列的操作仑荐。在真正核心底層方面好多文章是沒有具體去講的雕拼,您可以綜合性的看看其他的文章,推薦 Obj中國(guó) 上面關(guān)于多線程系統(tǒng)的講法释漆。好了悲没,我們繼續(xù):
進(jìn)度圖 progress graph
將 n 個(gè)并發(fā)線程的執(zhí)行抽象為一條 n 維 笛卡爾空間 中的軌跡線.
每條軸的 k 對(duì)應(yīng)線程 k 的進(jìn)度。每個(gè)點(diǎn)代表線程 k已經(jīng)完成了指令 I_k的狀態(tài)男图。
我們上面講的:
在多線程訪問同一個(gè)全局變量的時(shí)候示姿,我們查看對(duì)計(jì)數(shù)相關(guān)的匯編代碼,過程大致分為3步
我們這里設(shè)定在 A
線程的時(shí)候步驟為:
第一步 | 第二步 | 第三步 |
---|---|---|
A1 | A2 | A3 |
同理設(shè)定在 B
線程的時(shí)候步驟為:
第一步 | 第二步 | 第三步 |
---|---|---|
B1 | B2 | B3 |
這個(gè)時(shí)候我們來看下圖
我們看到圖中有一個(gè)點(diǎn) (A1,B3)
.
這個(gè)點(diǎn)的意思就是:當(dāng)線程 A
完了第 A1
狀態(tài)的同時(shí)逊笆,線程 B
完成了 B3
狀態(tài)栈戳。
使用進(jìn)度圖的目的就是講指令執(zhí)行模型轉(zhuǎn)化為從一種狀態(tài)到另一種狀態(tài)的轉(zhuǎn)換。
這樣就可以把程序的執(zhí)行歷史轉(zhuǎn)換為狀態(tài)空間中的一條軌跡線难裆。
對(duì)于線程不管是 A 或者 B 也好子檀,對(duì)全局變量的的操作(A1,A2,A3)步驟或者 (B1,B2,B3)步驟的過程中構(gòu)成了一個(gè)臨界區(qū),這個(gè)臨界區(qū)不應(yīng)該和其他進(jìn)程的臨界區(qū)交替執(zhí)行乃戈。我們確保每個(gè)線程在執(zhí)行它的臨界區(qū)中的指令時(shí)褂痰,擁有對(duì)共享變量 的 互斥 的訪問( Mutually exclusive access). 通常這種現(xiàn)象稱為互斥(Mutual exclusion).
這樣在上圖里面會(huì)出現(xiàn)這樣的規(guī)則:相同指令不能再同一時(shí)刻完成,對(duì)角線的線是不存在的症虑。
兩個(gè)臨界區(qū)的交集形成的狀態(tài)空間區(qū)域稱為不安全區(qū)(unsafe region
)
安全軌跡線:不在不安全區(qū)的軌跡線
不安全軌跡線:雷區(qū)的軌跡線
任何安全軌跡線都將正確地更新共享計(jì)數(shù)器缩歪。為了保證任意的全局變量在并發(fā)線程的正確執(zhí)行,我們就必須以某種方式同步線程谍憔,使他們總是有一條安全軌跡線匪蝙。其思想原理的基本思想就是基于 信號(hào)量
信號(hào)量: (semaphore) 一種特殊類型的變量。
信號(hào)量以s
表示.是具有非負(fù)整數(shù)值的全局變量习贫,只能有兩種特殊的操作來處理逛球,這兩種操作稱為 P 和 V:
P(s): 如果 s 是非零的,那么P 將 s 減1苫昌,并且立即返回颤绕。如果 S 為零,那么就掛起這個(gè)線程,直到 s 為零奥务,而一個(gè) V 操作會(huì)重啟這個(gè)線程涕烧。在重啟之后,P 操作將 s減1,并將控制返回給調(diào)用者汗洒。
V(s): V操作將 s 加1。如果有任何線程阻塞在 P 操作等待 s 變成非零,那么 V 操作會(huì)重啟這些線程中的一個(gè)父款,然后該線程將 s 減1溢谤,完成它的 P 操作。
P 中的測(cè)試和減1操作是不可分割的憨攒,一旦預(yù)測(cè)信號(hào)量s 變?yōu)榉橇闶郎保蜁?huì)將s減1,不能有中斷操作肝集,這個(gè)過程中不會(huì)有中斷瞻坝。 V 的加1 操作也是不可分割的。
沒有中斷的操作
加載 | 加1 | 存儲(chǔ)信號(hào) |
---|
ps: V 的定義中沒有定義等待線程被重啟動(dòng)的順序杏瞻。唯一的要求是 V 必須只能重啟一個(gè)正在等待的線程所刀。因此,當(dāng)有多個(gè)線程在等待同一個(gè)信號(hào)量時(shí)捞挥,就不能預(yù)測(cè) V 操作要重啟哪一個(gè)線程浮创。
P和V 的定義確保了一個(gè)正在運(yùn)行的程序絕不可能進(jìn)入這一種狀態(tài),也就是一個(gè)正確初始化了的信號(hào)量有一個(gè)負(fù)值砌函。這個(gè)屬性稱為 信號(hào)量不變性(semaphore invariant)
使用信號(hào)量來實(shí)現(xiàn)互斥
作用是:
將每個(gè)**全局變量 **與 一個(gè)信號(hào)量 s = 1
聯(lián)系起來斩披,然后用 P(s) 和 V(s) 操作將相應(yīng)的臨界區(qū)包圍起來。這種方式成為 二元信號(hào)量 (binary semaphore),它的值要么是 0
要么是 1
讹俊。以提供互斥為目的的二元型號(hào)量常常稱為 互斥鎖 (mutex).
那么在一個(gè)互斥鎖上執(zhí)行 P 操作稱為對(duì)互斥鎖加鎖垦沉。執(zhí)行 V操作稱為對(duì)互斥鎖解鎖。對(duì)一個(gè)互斥鎖加了鎖但是還沒有解鎖的線程稱為占用了這個(gè)互斥鎖仍劈。 一個(gè)被用作一組可用資源的計(jì)數(shù)器的信號(hào)量被稱為 計(jì)數(shù)信號(hào)量
厕倍。
如上面的雷區(qū)圖,在雷區(qū)內(nèi)因?yàn)樾盘?hào)量的不確定性故: s < 0
以上看到的仍然是坑: 因?yàn)樯厦媸菃翁幚砥鞯闹v解
但是有一個(gè)是萬用的:同步對(duì)共享變量的訪問是必須的耳奕。
多線程中對(duì)相同資源的訪問:
案例1:
在多媒體開發(fā)過程中對(duì)視頻的幀編碼绑青,并實(shí)時(shí)播放。這個(gè)時(shí)候就會(huì)有一個(gè)緩存的東西存在屋群,其存在的目的是為了減少視頻流的抖動(dòng)闸婴,引起的原因是幀的編碼與解碼時(shí)與數(shù)據(jù)相關(guān)的差異引起的。
案例2:
我們開發(fā)過程中對(duì)手機(jī)屏幕點(diǎn)擊事件的產(chǎn)生后芍躏,該事件先進(jìn)入緩存中邪乍,然后多線程根據(jù)優(yōu)先級(jí)來從緩沖區(qū)里面取出該事件進(jìn)行響應(yīng)。這就能很好解釋有時(shí)點(diǎn)擊屏幕卡屏了一會(huì)兒才響應(yīng)。
饑餓問題:
這個(gè)網(wǎng)上帖子泛濫: 傳送門 Obj中國(guó)
多個(gè)線程并行處理分配給它們的區(qū)域處理方法:
主線程給其他開的線程一個(gè)整數(shù)理解為該線程的 ID庇楞。每個(gè)線程用它的ID來決定它應(yīng)該計(jì)算序列的哪一部分榜配。
并行程序的性能
運(yùn)行時(shí)間是衡量程序性能的最終標(biāo)準(zhǔn)。相對(duì)衡量標(biāo)準(zhǔn)能夠說明并行程序有多好地利用了潛在的并行性。
并行程序的加速比(speedup)通常定義為: Sp = T1/Tp
p
是處理器的核樹畴博,Tk 是在 K
個(gè)核上的運(yùn)行時(shí)間可很。這個(gè)公式被稱為:強(qiáng)擴(kuò)展(strong scaling).
- 當(dāng)T1是程序順序執(zhí)行版本的執(zhí)行時(shí)間時(shí),Sp稱為 絕對(duì)加速比 (absolute speedup).
- 當(dāng)T1是程序并行版本在一個(gè)核上的執(zhí)行時(shí)間烙心,Sp稱為 相對(duì)加速比 (absolute speedup).
絕對(duì)加速比會(huì)比相對(duì)加速比更加難以測(cè)量,因?yàn)闇y(cè)量絕對(duì)加速比需要程序的兩種不同的版本乏沸。對(duì)于復(fù)雜的并行代碼淫茵,創(chuàng)造一個(gè)獨(dú)立的順序版本也不現(xiàn)實(shí)。
效率: Ep = Sp/p = T1/pTp
弱擴(kuò)展:(weak scaling): 在增加處理器數(shù)量的同時(shí)蹬跃,增加問題的規(guī)模匙瘪,這樣隨著處理器的數(shù)量的增加,每個(gè)處理器執(zhí)行的工作量保存不變蝶缀,在這樣的情況下加速比和效率被表達(dá)為單位時(shí)間完成的工作量丹喻。
線程安全
首先被稱為線程安全是當(dāng)且僅當(dāng)被多個(gè)多線程反復(fù)的調(diào)用,它才會(huì)一直產(chǎn)生正確的結(jié)果翁都。如果一個(gè)函數(shù)設(shè)計(jì)的不是線程安全的驻啤,它就是線程不安全的。
線程不安全的函數(shù)定義:
- 對(duì)全局變量的保護(hù)
- 保存跨越多個(gè)調(diào)用的狀態(tài)函數(shù)荐吵。如:隨機(jī)數(shù)生產(chǎn)的函數(shù)
- 返回指向靜態(tài)變量的指針的函數(shù)骑冗。
- 調(diào)用線程不安全函數(shù)的函數(shù)。
每個(gè)的具體例子有點(diǎn)多分下一章節(jié)進(jìn)行
iOS 一窺并發(fā)編程底層(二)