iOS進階之路 (十五)多線程 - 基礎

本篇主要涉及多線程的基礎知識,內容相對簡單,爲接下來的GCD、鎖作好鋪墊。html

一. 進程 & 線程 & 任務

1.1 進程 -- process

  • 進程是指在系統中正在運行的一個應用程序。
  • 每一個進程之間是獨立的,每一個進程均運行在其專用的且受保護的內存

補充:iOS系統是相對封閉的系統,App在各自的沙盒(sandbox)中運行,每一個App都只能讀取iPhone上系統爲該應用程序程序建立的文件夾AppData下的內容,不能隨意跨越本身的沙盒去訪問別的App沙盒中的內容。也就是說OS是單進程的,一個App就是一個進程。程序員

1.2 線程 - thread

  • 線程進程 的基本執行單元,一個 進程 的全部任務都在 線程 中執行
  • 進程 要想執行任務,至少要有一條 線程
  • 程序啓動會默認開啓一條 線程,這條線程被稱爲主線程UI線程

補充:對於iOS開發來講,線程的底層實現是基於 POSIX threads API 的,也就是咱們常說的 pthreads編程

1.3 任務:task

  • 通俗的說任務就是就一件事情或一段代碼,線程其實就是去執行這件事情。

1.4 進程與線程的關係

  • 地址空間: 同一進程的線程共享本進程的地址空間,而進程之間則是獨立的地址空間。
  • 資源擁有:同一進程內的線程共享本進程的資源如內存、I/O、cpu等,可是進程之間 的資源是獨立的。
  • 一個進程崩潰後,在保護模式下不會對其餘進程產生影響,可是一個線程崩潰整個進程 都死掉。因此多進程要比多線程健壯。
  • 進程切換時,消耗的資源大,效率高。因此涉及到頻繁的切換時,使用線程要好於進程。 一樣若是要求同時進行而且又要共享某些變量的併發操做,只能用線程不能用進程
  • 執行過程:每一個獨立的進程有一個程序運行的入口、順序執行序列和程序入口。可是線 程不能獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
  • 線程是處理器調度的基本單位,可是進程不是。

二. 線程和runloop的關係

蘋果不容許直接建立 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());
    }
複製代碼
  • 線程和 RunLoop 之間是一一對應的,其關係是保存在一個全局的 Dictionary 裏, key 是 pthread_t, value 是 CFRunLoopRef。
  • RunLoop 的建立是發生在第一次獲取時,RunLoop 的銷燬是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)
  • 主線程runloop 程序一啓動就默認建立好了,默認開啓
  • 子線程runloop 只有當咱們使用的時候纔會建立,默認關閉,因此在子線程調用runloop方法要開啓runloop。

三. 多線程

在同一時刻,一個CPU只能處理1條線程,但CPU能夠在多條線程之間快速的切換,只要切換的足夠快,就形成了多線程一同執行的假象。安全

3.1 多線程的意義

  1. 優勢
  • 能適當提升程序的執行效率
  • 能適當提升資源的利用率(CPU,內存)
  • 線程上的任務執行完成後,線程會自動銷燬
  1. 缺點
  • 開啓線程須要佔用必定的內存空間(默認狀況下,每個線程都佔 512 KB)
  • 若是開啓大量的線程,會佔用大量的內存空間,下降程序的性能 * 線程越多,CPU 在調用線程上的開銷就越大
  • 程序設計更加複雜,好比線程間的通訊、多線程的數據共享

3.2 多線程的生命週期

  • 新建:實例化線程對象
  • 就緒:向線程對象發送start消息,線程對象被加入可調度線程池等待CPU調度。
  • 運行:CPU 負責調度可調度線程池中線程的執行。線程執行完成以前,狀態可能會在就緒和運行之間來回切換。就緒和運行之間的狀態變化由CPU負責,程序員不能干預。
  • 阻塞:當知足某個預約條件時,可使用休眠或鎖,阻塞線程執行。sleepForTimeInterval(休眠指定時長),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥鎖)。
  • 死亡:正常死亡,線程執行完畢。非正常死亡,當知足某個條件後,在線程內部停止執行/在主線程停止線程對象

3.3 線程池 - thread pool

線程池是一種線程使用模式。 線程過多會帶來調度開銷,進而影響緩存局部性和總體性能。 而線程池維護着多個線程,等待着監督管理者分配可併發執行的任務。 這避免了在處理短期任務時建立與銷燬線程的代價。bash

線程池的執行流程如圖下:多線程

  1. 線程池大小 小於 核心線程池大小,建立線程執行任務
  2. 線程池大小 大於等於 核心線程池大小,則判斷線程池工做隊列是否已滿
  • 若沒滿就將任務提交給工做隊列
  • 若已滿時,將建立新的線程來執行任務;反之則交給 飽和策略 去處理。

飽和策略:併發

  • AbortPolicy 直接拋出RejectedExecutionExeception 異常來阻止系統正常運行
  • CallerRunsPolicy 將任務回退到調用者
  • DisOldestPolicy 丟掉等待最久的任務‘
  • DisCardPolicy 直接丟棄任務
  • 這四種拒絕策略均實現的RejectedExecutionHandler接口

因此在併發的時候,同時能有多少個線程在運行是由線程池的線程緩存數量決定。GCD和NSOperation的線程池緩存數量都是64條app

3.4 多線程的實現方案

  • GCD僅僅支持FIFO隊列,不支持異步操做之間的依賴關係設置。而NSOperation中的隊列能夠被從新設置優先級,從而實現不一樣操做的執行順序調整
  • NSOperation支持KVO,能夠觀察任務的執行狀態
  • GCD更接近底層,GCD在追求性能的底層操做來講,是速度最快的
  • 從異步操做之間的事務性,順序行,依賴關係。GCD須要本身寫更多的代碼來實現,而NSOperation已經內建了這些支持
  • 若是異步操做的過程須要更多的被交互和UI呈現出來,NSOperation更好;底層代碼中,任務之間不太互相依賴,而須要更高的併發能力,GCD則更有優點。

四. 線程的同步

線程編程的危害之一是在多個線程之間的資源爭奪。若是多個線程在同一個時間試圖使用或者修改同一個資源,就會出現問題。緩解該問題的方法之一是消除共享資源,並確保每一個線程都有在它操做的資源上面的獨特設置。由於保持徹底獨立的資源是不可行的,因此你可能必須使用鎖,條件,原子操做和其餘技術來同步資源的訪問。異步

咱們看一下蘋果官方給出的線程同步工具:

4.1 Atomic Operations -- 原子操做

Atomic Operations是一種基於基本數據類型的同步形式,底層用匯編鎖來控制變量的變化,保證數據的正確性,好處在於不會block互相競爭的線程,且相比鎖耗時不多。

4.2 Memory Barriers -- 內存屏障

爲了達到最佳性能,編譯器一般會講彙編級別的指令進行從新排序,從而保持處理器的指令管道盡量的滿。做爲優化的一部分,編譯器可能會對內存訪問的指令進行從新排序(在它認爲不會影響數據的正確性的前提下),然而,這並不必定都是正確的,順序的變化可能致使一些變量的值獲得不正確的結果。

Memory Barriers是一種不會形成線程block的同步工具,它用於確保內存操做的正確順序。Memory Barriers像一道屏障,迫使處理器在其前面完成必須的加載或者存儲的操做。Memory Barriers常被用於確保一個線程中可被其餘線程訪問的內存操做按照預期的順序執行。具體參考Memory Barriers。

在程序中應用Memory Barriers只須要在指定地方調用:

OSMemoryBarrier();
複製代碼

4.3 Volatile Variables -- 揮發變量

Volatile Variables是另一種針對變量的同步工具。衆所周知,CPU訪問寄存器的速度比訪問內存速度快不少,所以,CPU有時候會將一些變量放置到寄存器中,而不是每次都從內存中讀取(例如for循環中的i值)從而優化代碼,可是可能會致使錯誤。 例如,一個線程在CPUA中被處理,CPUA從內存獲取變量F的值,此時,並無其餘CPU用到變量F,因此CPUA將變量F存到寄存器中,方便下次使用,此時,另外一個線程在CPUB中被處理,CPUB從內存中獲取變量F的值,改變該值後,更新內存中的F值。可是,因爲CPUA每次都只會從寄存器中取F的值,而不會再次從內存中取,因此,CPUA處理後的結果就是不正確的。

對一個變量加上Volatile關鍵字能夠迫使編譯器每次都從新從內存中加載該變量,而不會從寄存器中加載。當一個變量的值可能隨時會被一個外部源改變時,應該將該變量聲明爲Volatile。

4.4 Locks -- 鎖

Locks是一種最經常使用的同步工具。Locks能夠對一段代碼進行保護,保證同時只有一個線程在執行該段代碼。

關於iOS開發中的各類鎖的性能和使用,咱們後續單獨開一個篇章詳細學習。

4.5 Conditions -- 條件

Conditions是一種特殊的lock,用於同步操做的順序。與Mutex Lock不一樣的是,一個等待Condition的線程保持block,直到另外一個線程顯示對該Condition調用signal。

因爲操做系統的緣由,Conditions可能會獲得一些不正確的信號,爲了不這類問題,能夠在使用Conditions時,加入Predicate(斷言)。Predicate是一種有效地判斷是否讓一個線程處理信號的方式。Conditions保持線程休眠,直到另外一個線程調用signal,並設置了Predicate。

4.6 perform selector routines

cocoa應用能夠用一種便利而同步的方式向線程傳遞消息,NSObjec對象聲明瞭在線程上執行selector的方法,這些方法異步地傳遞消息,而系統確保會同步地在目標線程上執行這些selector,每一個請求都會在目標線程的runloop上排上隊,並按收到的順序進行執行。

五. 線程間通訊

線程間通訊的表現爲:

  • 一個線程傳遞數據給另外一個線程;
  • 在一個線程中執行完特定任務後,轉到另外一個線程繼續執行任務。

先看下官方文檔推薦的線程通訊方案:

  1. 直接消息傳遞: 經過 performSelector 的一系列方法,能夠實現由某一線程指定在另外的線程上執行任務。由於任務的執行上下文是目標線程,這種方式發送的消息將會自動的被序列化
  2. 全局變量、共享內存塊和對象: 在兩個線程之間傳遞信息的另外一種簡單方法是使用全局變量,共享對象或共享內存塊。儘管共享變量既快速又簡單,可是它們比直接消息傳遞更脆弱。必須使用鎖或其餘同步機制仔細保護共享變量,以確保代碼的正確性。 不然可能會致使競爭情況,數據損壞或崩潰。
  3. 條件執行: 條件是一種同步工具,可用於控制線程什麼時候執行代碼的特定部分。您能夠將條件視爲關守,讓線程僅在知足指定條件時運行。
  4. Runloop sources: 一個自定義的 Runloop source 配置可讓一個線程上收到特定的應用程序消息。因爲 Runloop source 是事件驅動的,所以在無事可作時,線程會自動進入睡眠狀態,從而提升了線程的效率
  5. Ports and sockets:基於端口的通訊是在兩個線程之間進行通訊的一種更爲複雜的方法,但它也是一種很是可靠的技術。更重要的是,端口和套接字可用於與外部實體(例如其餘進程和服務)進行通訊。爲了提升效率,使用 Runloop source 來實現端口,所以當端口上沒有數據等待時,線程將進入睡眠狀態
  6. 消息隊列: 傳統的多處理服務定義了先進先出(FIFO)隊列抽象,用於管理傳入和傳出數據。儘管消息隊列既簡單又方便,可是它們不如其餘一些通訊技術高效
  7. Cocoa 分佈式對象: 分佈式對象是一種 Cocoa 技術,可提供基於端口的通訊的高級實現。儘管能夠將這種技術用於線程間通訊,可是強烈建議不要這樣作,由於它會產生大量開銷。分佈式對象更適合與其餘進程進行通訊,儘管在這些進程之間進行事務的開銷也很高

我的經常使用的通訊方案有:

5.1 NSThread 線程間通訊

NSThread這套方案是通過蘋果封裝後,而且徹底面向對象的。不過它的生命週期仍是須要咱們手動管理,因此實際上使用也比較少。

  1. performSelectorOnMainThread
//數據請求完畢回調到主線程,更新UI資源信息  waitUntilDone  設置YES ,表明等待當前線程執行完畢
[self performSelectorOnMainThread:@selector(dothing:) withObject:@[@"1"] waitUntilDone:YES];
複製代碼
  1. performSelectorInBackground
//將當前的邏輯轉到後臺線程去執行
[self performSelectorInBackground:@selector(dothing:) withObject:@[@"2"]];
複製代碼
  1. 本身定義線程,將當前數據轉移到指定的線程內去通訊操做
//支持自定義線程通訊執行相應的操做
NSThread * thread = [[NSThread alloc]initWithTarget:self selector:@selector(entryThreadPoint) object:nil];
[thread start];
//當咱們須要在特定的線程內去執行某一些數據的時候,咱們須要指定某一個線程操做
[self performSelector:@selector(dothing:) onThread:thread withObject:nil waitUntilDone:YES];
複製代碼

5.2 GCD 線程間通訊

  1. 須要更新UI操做的時候使用下面這個GCD的block方法
//回到主線程更新UI操做
dispatch_async(dispatch_get_main_queue(), ^{
    //數據執行完畢回調到主線程操做UI更新數據
});
複製代碼
  1. 有時候省去麻煩,咱們使用系統的全局隊列:通常用這個處理遍歷大數據查詢操做
DISPATCH_QUEUE_PRIORITY_HIGH  全局隊列高優先級
DISPATCH_QUEUE_PRIORITY_LOW 全局隊列低優先級
DISPATCH_QUEUE_PRIORITY_BACKGROUND  全局隊裏後臺執行隊列
// 全局併發隊列執行處理大量邏輯時使用   
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

});
複製代碼
  1. 當在開發中遇到一些數據須要單線程訪問的時候,咱們能夠採起同步線程的作法,來保證數據的正常執行
//當咱們須要執行一些數據安全操做寫入的時候,須要同步操做,後面全部的任務要等待當前線程的執行
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
    //同步線程操做能夠保證數據的安全完整性
});
複製代碼

5.3 NSOperation 線程間通訊

if ([[NSThread currentThread] isMainThread]) {
    NSLog(@"## 我是主線程 能夠更新UI ##");
} else {
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        NSLog(@"### 我是在主隊列執行的block ####");
    }];
}
複製代碼

六. 總結

本篇參照官方文檔,學習了多線程的基礎知識,下篇開始學習宏大的中央調度系統 - GCD。

參考資料

蘋果官方文檔 -- Threading Programming Guide

jackyshan -- iOS多線程詳解:概念篇

YI_LIN -- 線程同步詳解

我是好寶寶 -- iOS探索 多線程原理

相關文章
相關標籤/搜索