本篇主要涉及多線程的基礎知識,內容相對簡單,爲接下來的GCD、鎖作好鋪墊。html
進程
是指在系統中正在運行的一個應用程序。進程
之間是獨立的,每一個進程
均運行在其專用的且受保護的內存補充:iOS系統是相對封閉的系統,App在各自的沙盒(sandbox)中運行,每一個App都只能讀取iPhone上系統爲該應用程序程序建立的文件夾AppData下的內容,不能隨意跨越本身的沙盒去訪問別的App沙盒中的內容。也就是說OS是單進程的,一個App就是一個進程。程序員
線程
是 進程
的基本執行單元,一個 進程
的全部任務都在 線程
中執行進程
要想執行任務,至少要有一條 線程
線程
,這條線程被稱爲主線程
或 UI線程
補充:對於iOS開發來講,線程的底層實現是基於 POSIX threads API 的,也就是咱們常說的 pthreads
;編程
蘋果不容許直接建立 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain()和 CFRunLoopGetCurrent()。 這兩個函數內部的邏輯大概是下面這樣:緩存
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進入時,初始化全局Dic,並先爲主線程建立一個 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 裏獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時,建立一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 註冊一個回調,當線程銷燬時,順便也銷燬其對應的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
複製代碼
在同一時刻,一個CPU只能處理1條線程,但CPU能夠在多條線程之間快速的切換,只要切換的足夠快,就形成了多線程一同執行的假象。安全
新建
:實例化線程對象就緒
:向線程對象發送start消息,線程對象被加入可調度線程池等待CPU調度。運行
:CPU 負責調度可調度線程池中線程的執行。線程執行完成以前,狀態可能會在就緒和運行之間來回切換。就緒和運行之間的狀態變化由CPU負責,程序員不能干預。阻塞
:當知足某個預約條件時,可使用休眠或鎖,阻塞線程執行。sleepForTimeInterval(休眠指定時長),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥鎖)。死亡
:正常死亡,線程執行完畢。非正常死亡,當知足某個條件後,在線程內部停止執行/在主線程停止線程對象線程池是一種線程使用模式。 線程過多會帶來調度開銷,進而影響緩存局部性和總體性能。 而線程池維護着多個線程,等待着監督管理者分配可併發執行的任務。 這避免了在處理短期任務時建立與銷燬線程的代價。bash
線程池的執行流程如圖下:多線程
線程池大小
小於 核心線程池大小
,建立線程執行任務線程池大小
大於等於 核心線程池大小
,則判斷線程池工做隊列是否已滿飽和策略
去處理。飽和策略:併發
AbortPolicy
直接拋出RejectedExecutionExeception 異常來阻止系統正常運行CallerRunsPolicy
將任務回退到調用者DisOldestPolicy
丟掉等待最久的任務‘DisCardPolicy
直接丟棄任務因此在併發的時候,同時能有多少個線程在運行是由線程池的線程緩存數量決定。GCD和NSOperation的線程池緩存數量都是64條app
線程編程的危害之一是在多個線程之間的資源爭奪。若是多個線程在同一個時間試圖使用或者修改同一個資源,就會出現問題。緩解該問題的方法之一是消除共享資源,並確保每一個線程都有在它操做的資源上面的獨特設置。由於保持徹底獨立的資源是不可行的,因此你可能必須使用鎖,條件,原子操做和其餘技術來同步資源的訪問。異步
咱們看一下蘋果官方給出的線程同步工具:
Atomic Operations是一種基於基本數據類型的同步形式,底層用匯編鎖來控制變量的變化,保證數據的正確性,好處在於不會block互相競爭的線程,且相比鎖耗時不多。
爲了達到最佳性能,編譯器一般會講彙編級別的指令進行從新排序,從而保持處理器的指令管道盡量的滿。做爲優化的一部分,編譯器可能會對內存訪問的指令進行從新排序(在它認爲不會影響數據的正確性的前提下),然而,這並不必定都是正確的,順序的變化可能致使一些變量的值獲得不正確的結果。
Memory Barriers是一種不會形成線程block的同步工具,它用於確保內存操做的正確順序。Memory Barriers像一道屏障,迫使處理器在其前面完成必須的加載或者存儲的操做。Memory Barriers常被用於確保一個線程中可被其餘線程訪問的內存操做按照預期的順序執行。具體參考Memory Barriers。
在程序中應用Memory Barriers只須要在指定地方調用:
OSMemoryBarrier();
複製代碼
Volatile Variables是另一種針對變量的同步工具。衆所周知,CPU訪問寄存器的速度比訪問內存速度快不少,所以,CPU有時候會將一些變量放置到寄存器中,而不是每次都從內存中讀取(例如for循環中的i值)從而優化代碼,可是可能會致使錯誤。 例如,一個線程在CPUA中被處理,CPUA從內存獲取變量F的值,此時,並無其餘CPU用到變量F,因此CPUA將變量F存到寄存器中,方便下次使用,此時,另外一個線程在CPUB中被處理,CPUB從內存中獲取變量F的值,改變該值後,更新內存中的F值。可是,因爲CPUA每次都只會從寄存器中取F的值,而不會再次從內存中取,因此,CPUA處理後的結果就是不正確的。
對一個變量加上Volatile關鍵字能夠迫使編譯器每次都從新從內存中加載該變量,而不會從寄存器中加載。當一個變量的值可能隨時會被一個外部源改變時,應該將該變量聲明爲Volatile。
Locks是一種最經常使用的同步工具。Locks能夠對一段代碼進行保護,保證同時只有一個線程在執行該段代碼。
關於iOS開發中的各類鎖的性能和使用,咱們後續單獨開一個篇章詳細學習。Conditions是一種特殊的lock,用於同步操做的順序。與Mutex Lock不一樣的是,一個等待Condition的線程保持block,直到另外一個線程顯示對該Condition調用signal。
因爲操做系統的緣由,Conditions可能會獲得一些不正確的信號,爲了不這類問題,能夠在使用Conditions時,加入Predicate(斷言)。Predicate是一種有效地判斷是否讓一個線程處理信號的方式。Conditions保持線程休眠,直到另外一個線程調用signal,並設置了Predicate。
cocoa應用能夠用一種便利而同步的方式向線程傳遞消息,NSObjec對象聲明瞭在線程上執行selector的方法,這些方法異步地傳遞消息,而系統確保會同步地在目標線程上執行這些selector,每一個請求都會在目標線程的runloop上排上隊,並按收到的順序進行執行。
線程間通訊的表現爲:
先看下官方文檔推薦的線程通訊方案:
直接消息傳遞
: 經過 performSelector
的一系列方法,能夠實現由某一線程指定在另外的線程上執行任務。由於任務的執行上下文是目標線程,這種方式發送的消息將會自動的被序列化全局變量、共享內存塊和對象
: 在兩個線程之間傳遞信息的另外一種簡單方法是使用全局變量,共享對象或共享內存塊。儘管共享變量既快速又簡單,可是它們比直接消息傳遞更脆弱。必須使用鎖或其餘同步機制仔細保護共享變量,以確保代碼的正確性。 不然可能會致使競爭情況,數據損壞或崩潰。條件執行
: 條件是一種同步工具,可用於控制線程什麼時候執行代碼的特定部分。您能夠將條件視爲關守,讓線程僅在知足指定條件時運行。Runloop sources
: 一個自定義的 Runloop source 配置可讓一個線程上收到特定的應用程序消息。因爲 Runloop source 是事件驅動的,所以在無事可作時,線程會自動進入睡眠狀態,從而提升了線程的效率Ports and sockets
:基於端口的通訊是在兩個線程之間進行通訊的一種更爲複雜的方法,但它也是一種很是可靠的技術。更重要的是,端口和套接字可用於與外部實體(例如其餘進程和服務)進行通訊。爲了提升效率,使用 Runloop source 來實現端口,所以當端口上沒有數據等待時,線程將進入睡眠狀態消息隊列
: 傳統的多處理服務定義了先進先出(FIFO)隊列抽象,用於管理傳入和傳出數據。儘管消息隊列既簡單又方便,可是它們不如其餘一些通訊技術高效Cocoa 分佈式對象
: 分佈式對象是一種 Cocoa 技術,可提供基於端口的通訊的高級實現。儘管能夠將這種技術用於線程間通訊,可是強烈建議不要這樣作,由於它會產生大量開銷。分佈式對象更適合與其餘進程進行通訊,儘管在這些進程之間進行事務的開銷也很高我的經常使用的通訊方案有:
NSThread這套方案是通過蘋果封裝後,而且徹底面向對象的。不過它的生命週期仍是須要咱們手動管理,因此實際上使用也比較少。
//數據請求完畢回調到主線程,更新UI資源信息 waitUntilDone 設置YES ,表明等待當前線程執行完畢
[self performSelectorOnMainThread:@selector(dothing:) withObject:@[@"1"] waitUntilDone:YES];
複製代碼
//將當前的邏輯轉到後臺線程去執行
[self performSelectorInBackground:@selector(dothing:) withObject:@[@"2"]];
複製代碼
//支持自定義線程通訊執行相應的操做
NSThread * thread = [[NSThread alloc]initWithTarget:self selector:@selector(entryThreadPoint) object:nil];
[thread start];
//當咱們須要在特定的線程內去執行某一些數據的時候,咱們須要指定某一個線程操做
[self performSelector:@selector(dothing:) onThread:thread withObject:nil waitUntilDone:YES];
複製代碼
//回到主線程更新UI操做
dispatch_async(dispatch_get_main_queue(), ^{
//數據執行完畢回調到主線程操做UI更新數據
});
複製代碼
DISPATCH_QUEUE_PRIORITY_HIGH 全局隊列高優先級
DISPATCH_QUEUE_PRIORITY_LOW 全局隊列低優先級
DISPATCH_QUEUE_PRIORITY_BACKGROUND 全局隊裏後臺執行隊列
// 全局併發隊列執行處理大量邏輯時使用
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});
複製代碼
//當咱們須要執行一些數據安全操做寫入的時候,須要同步操做,後面全部的任務要等待當前線程的執行
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
//同步線程操做能夠保證數據的安全完整性
});
複製代碼
if ([[NSThread currentThread] isMainThread]) {
NSLog(@"## 我是主線程 能夠更新UI ##");
} else {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"### 我是在主隊列執行的block ####");
}];
}
複製代碼
本篇參照官方文檔,學習了多線程的基礎知識,下篇開始學習宏大的中央調度系統 - GCD。