iOS多線程基礎(想不會都難)

標籤(空格分隔): iOS多線程 NSThread NSOpearation GCDios


本文是在簡述做者:開發者zuoios多線程系列文章基礎上我的的補充,感謝原做!程序員


圖片來自互聯網
#第一部分 多線程基礎 ##1、線程的基本概念 ###1.多線程出現的背景 在計算機編程中,一個基本的概念就是同時對多個任務加以控制。許多程序設計問題都要求程序可以停下手頭的工做,改成處理其餘一些問題,再返回主進程。能夠經過多種途徑達到這個目的。多線程是爲了同步完成多項任務,不是爲了提升運行效率,而是爲了經過提升資源使用效率來提升系統整體的效率。線程是在同一時間須要完成多項任務的時候執行的。

###2.進程面試

進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操做系統結構的基礎。編程

簡單來講,進程是指在系統中正在運行的一個應用程序,每個程序都是一個進程,而且進程之間是獨立的,每一個進程均運行在其專用且受保護的內存空間內。設計模式

###3.線程安全

線程,是程序執行流的最小單元線程是程序中一個單一的順序控制流程。是進程內一個相對獨立的、可調度的執行單元,是系統獨立調度和分派CPU的基本單位指運行中的程序的調度單位。bash

簡單來講,1個進程要想執行任務,必須得有線程。服務器

線程中任務的執行是串行的 要在1個線程中執行多個任務,那麼只能一個一個地按順序執行這些任務 也就是說,在同一時間內,1個線程只能執行1個任務 由此能夠理解線程是進程中的1條執行路徑網絡

###4.多線程多線程

多線程(multithreading),是指從軟件或者硬件上實現多個線程併發執行的技術。具備多線程能力的計算機因有硬件支持而可以在同一時間執行多於一個線程,進而提高總體處理性能。

原理:

  • 同一時間,CPU只能處理1條線程,只有1條線程在工做(執行)
  • 多線程併發(同時)執行,實際上是CPU快速地在多條線程之間調度(切換)
  • 若是CPU調度線程的時間足夠快,就形成了多線程併發執行的假象 注意:多線程併發,並非cpu在同一時刻同時執行多個任務,只是CPU調度足夠快,形成的假象。

優勢:

  • 能適當提升程序的執行效率
  • 能適當提升資源利用率(CPU、內存利用率)

缺點:

  • 開啓線程須要佔用必定的內存空間(默認狀況下,主線程佔用1M,子線程佔用512KB),若是開啓大量的線程,會佔用大量的內存空間,下降程序的性能
  • 線程越多,CPU在調度線程上的開銷就越大

##2、iOS開發中的應用 ###1.主線程 一個iOS程序運行後,默認會開啓1條線程,稱爲「主線程」或「UI線程」。

做用: 顯示\刷新UI界面 處理UI事件(好比點擊事件、滾動事件、拖拽事件等) 注意: 刷新UI必須放在主線程 別將比較耗時的操做放到主線程中 耗時操做會卡住主線程,嚴重影響UI的流暢度 ###2.實現方案

實現方案


#第二部分 NSThread 先看一段API文檔的描述 An NSThread object controls a thread of execution. Use this class when you want to have an Objective-C method run in its own thread of execution. Threads are especially useful when you need to perform a lengthy task, but don’t want it to block the execution of the rest of the application. In particular, you can use threads to avoid blocking the main thread of the application, which handles user interface and event-related actions. Threads can also be used to divide a large job into several smaller jobs, which can lead to performance increases on multi-core computers.

大概的意思是:一個NSThread對象管理一個線程的執行。當你想要將一個Objective-C方法運行在它本身獨立的線程中,可使用這個類。當你想執行一個比較耗時(冗長)的操做而又不想阻塞程序其餘部分的運行狀態時,線程是特別有用的。尤爲是你可使用線程來避免阻塞主線程處理用戶界面以及和事件相關的活動。線程能夠將待處理任務分割成小任務以提升多核計算機的性能。 ##1、NSThread的使用

###1.線程的建立

方式一:

/ *  建立並啓動線程
     *
     *  參數1要執行的方法
     *  參數2提供selector的對象,一般是self
     *  參數3傳遞給selector的參數
     */
    [NSThread detachNewThreadSelector:(nonnull SEL)> toTarget:(nonnull id) withObject:(nullable id)]
複製代碼

方式二:

//參數一:提供selector的對象,一般是self,參數2:要執行的方法,參數3:傳遞給selector的參數(若是selector方法不帶參數,就使用nil)
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(doSomething) object:nil];
複製代碼

方式三:

//隱式建立並啓動線程,第一個參數爲調用的方法,第二個參數爲傳給selector方法的參數
- (void)performSelectorInBackground:(SEL)aSelector
                         withObject:(id)arg
複製代碼

NSThread對象的常見屬性

//只讀屬性,線程是否在執行
    thread.isExecuting;
    //只讀屬性,線程是否被取消
    thread.isCancelled;
    //只讀屬性,線程是否完成
    thread.isFinished;
    //是不是主線程
    thread.isMainThread;
    
    //線程的優先級,取值範圍0.0到1.0,默認優先級0.5,1.0表示最高優     //先級,優先級高,CPU調度的頻率高
    thread.threadPriority;
    
    //線程的堆棧大小,線程執行前堆棧大小爲512K,線程完成後堆棧大小       爲0K
    //注意:線程執行完畢後,因爲內存空間被釋放,不能再次啓動
    thread.stackSize;
複製代碼

NSThread對象的方法

//線程開始,線程加入線程池等待CPU調度(並不是真正開始執行,只是一般等待時間都很是短,看不出效果)
    [thread start];
    if(!thread.isCancelled){//在執行以前須要先確認線程狀態,若是已經取消就直接返回
        [thread cancel]; //通知線程取消,能夠在外不終止線程執行
    }else{
        return;
    }
複製代碼

###2.NSThread的類方法 類方法都用在線程內部,也就是說類方法做用於包含本行類方法的線程。

<1>當前線程,在開發中經常使用於調試,適用於全部多線程計數,返回一個線程號碼

//number == 1 表示主線程,number != 1表示後臺線程
int number = [NSThread currentThread];
複製代碼

<2>阻塞方法

//休眠到指定時間
[NSThread sleepUntilDate:[NSDate date]];
//休眠指定時長
[NSThread sleepForTimeInterval:4.5];
複製代碼

<3>其餘類方法

//退出線程
[NSThread exit];
//當前線程是否爲主線程
[NSThread isMainThread];
//是否多線程
[NSThread isMultiThreaded];
//返回主線程的對象
NSThread *mainThread = [NSThread mainThread];
複製代碼

###3.線程的狀態

線程的狀態
] <1>新建:實例化對象 <2>就緒:向線程對象發送 start 消息,線程對象被加入「可調度線程池」等待 CPU 調度;detach 方法和 performSelectorInBackground 方法會直接實例化一個線程對象並加入「可調度線程池」 <3>運行:CPU 負責調度「可調度線程池」中線程的執行,線程執行完成以前,狀態可能會在「就緒」和「運行」之間來回切換,「就緒」和「運行」之間的狀態變化由 CPU 負責,程序員不能干預 <4>阻塞:當知足某個預約條件時,可使用休眠或鎖阻塞線程執行,影響的方法有:sleepForTimeInterval,sleepUntilDate,@synchronized(self)x線程鎖; 線程對象進入阻塞狀態後,會被從「可調度線程池」中移出,CPU 再也不調度 <5>死亡 死亡方式

正常死亡:線程執行完畢
非正常死亡:線程內死亡--->[NSThread exit]:強行停止後,後續代碼都不會在執行
線程外死亡:[threadObj cancel]--->通知線程對象取消,在線程執行方法中須要增長 isCancelled 判斷,若是 isCancelled == YES,直接返回
複製代碼

死亡後線程對象的 isFinished 屬性爲 YES;若是是發送 calcel 消息,線程對象的 isCancelled 屬性爲YES;死亡後 stackSize == 0,內存空間被釋放。

###4.多線程的安全問題 多個線程訪問同一塊資源進行讀寫,若是不加控制隨意訪問容易產生數據錯亂從而引起數據安全問題。爲了解決這一問題,就有了加鎖的概念。加鎖的原理就是當有一個線程正在訪問資源進行寫的時候,不容許其餘線程再訪問該資源,只有當該線程訪問結束後,其餘線程才能按順序進行訪問。對於讀取數據,有些程序設計是容許多線程同時讀的,有些不容許。

解決多線程安全問題 <1>互斥鎖

// 注意:鎖定1份代碼只用1把鎖,用多把鎖是無效的
@synchronized(鎖對象) { // 須要鎖定的代碼  }
複製代碼

使用互斥鎖,在同一個時間,只容許一條線程執行鎖中的代碼.由於互斥鎖的代價很是昂貴,因此鎖定的代碼範圍應該儘量小,只要鎖住資源讀寫部分的代碼便可。使用互斥鎖也會影響併發的目的。

<2>原子屬性

@property (strong, nonatomic) UIWindow *window;
複製代碼

atomic:可以實現「單寫多讀」的數據保護,同一時間只容許一個線程修改屬性值,可是容許多個線程同時讀取屬性值,在多線程讀取數據時,有可能出現「髒」數據 - 讀取的數據可能會不正確。原子屬性是默認屬性,若是不須要考慮線程安全,要指定 nonatomic。

atomic(原子屬性)在setter方法內部加了一把自旋鎖 nonatomic(非原子屬性)下,set和get方法都不會加鎖,消耗資源小適合內存小的移動設備

UIKit中幾乎全部控件都不是線程安全的,所以須要在主線程上更新UI

原子屬性內部使用的 自旋鎖 自旋鎖和互斥鎖的區別

共同點: 均可以鎖定一段代碼。 同一時間, 只有線程可以執行這段鎖定的代碼

區別:互斥鎖,在鎖定的時候,其餘線程會睡眠,等待條件知足,再喚醒
自旋鎖,在鎖定的時候, 其餘的線程會作死循環,一直等待這條件知足,一旦條件知足,立馬去執行,少了一個喚醒過程
複製代碼

// 在主線程更新UI,有什麼好處?

  1. 只在主線程更新UI,就不會出現多個線程同時改變 同一個UI控件
  2. 主線程的優先級最高。也就意味UI的更新優先級高。 會讓用戶感受很流暢

開發建議

1.全部屬性都聲明爲nonatomic 2.儘可能避免多線程搶奪同一塊資源 3.儘可能將加鎖、資源搶奪的業務邏輯交給服務器端處理,減少移動客戶端的壓力

###5.自動釋放池和運行循環 <1>運行循環 做用:保證程序不退出,堅挺全部事件,例如:手勢觸摸,網絡加載等 特性:沒有事件時,會休眠(省電),一旦監聽到事件,會當即響應,每個線程都有一個 runloop,可是隻有主線程的 runloop 會默認啓動。

<2>自動釋放池 工做原理:自動釋放池被銷燬或耗盡時會向池中全部對象發送 release 消息,釋放全部 autorelease 的對象! 建立和銷燬:每一次運行循環啓動後會建立自動釋放池;程序執行過程當中,全部 autorelease 對象在出了做用域以後,會被添加到最近建立的自動釋放池中;運行循環結束前,會釋放自動釋放池。 自動釋放池在ARC中一樣須要。 工做原理圖:

Screen Shot 2016-05-03 at 20.54.21.png

常見面試題:

int largeNumber = 2 * 1024 * 1024; // 問題:(1)如下代碼是否存在問題?(2)若是有,怎麼修改?

for (int i = 0; i < largeNumber; i++) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"Hello "];
        str = [str uppercaseString];
        str = [str stringByAppendingString:@" - World"];
    }
}
複製代碼

網上的解決辦法: 1)@autoreleasepool 放在外面,保證循環以後釋放循環中的自動釋放對象 2)@autoreleasepool 放在內部,每一次循環以後,都傾倒一次自動釋放池,內存管理是最好的,可是性能很差!

###6.線程通訊(方法繼承自NSObject)

Screen Shot 2016-05-04 at 00.09.18.png

//在主線程上執行操做,例如給UIImageVIew設置圖片
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
//在指定線程上執行操做
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wai
複製代碼

#第三部分 GCD ##1、GCD簡介 GCD(Grand Central Dispatch) 偉大的中央調度系統,是蘋果爲多核並行運算提出的C語言併發技術框架。

GCD會自動利用更多的CPU內核; 會自動管理線程的生命週期(建立線程,調度任務,銷燬線程等); 程序員只須要告訴 GCD 想要如何執行什麼任務,不須要編寫任何線程管理代碼

一些專業術語

dispatch :派遣/調度
    
queue:隊列
    用來存聽任務的先進先出(FIFO)的容器
sync:同步
    只是在當前線程中執行任務,不具有開啓新線程的能力
async:異步
    能夠在新的線程中執行任務,具有開啓新線程的能力
concurrent:併發
    多個任務併發(同時)執行
串行:
    一個任務執行完畢後,再執行下一個任務
複製代碼

##2、GCD中的核心概念 ###1.任務 任務就是要在線程中執行的操做。咱們須要將要執行的代碼用block封裝好,而後將任務添加到隊列並指定任務的執行方式,等待CPU從隊列中取出任務放到對應的線程中執行。

- queue:隊列
 - block:任務
// 1.用同步的方式執行任務
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

// 2.用異步的方式執行任務
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

// 3.GCD中還有個用來執行任務的函數
// 在前面的任務執行結束後它才執行,並且它後面的任務等它執行完成以後纔會執行
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
複製代碼

###2.隊列 隊列以先進先出按照執行方式(併發/串行)調度任務在對應的線程上執行; 隊列分爲:自定義隊列、主隊列和全局隊列;

<1>自定義隊列 自定義隊列又分爲:串行隊列和併發隊列 串行隊列 串行隊列一次只調度一個任務,一個任務完成後再調度下一個任務

// 1.使用dispatch_queue_create函數建立串行隊列
// 建立串行隊列(隊列類型傳遞NULL或者DISPATCH_QUEUE_SERIAL)
dispatch_queue_t queue = dispatch_queue_create("queue", NULL);

// 2.使用dispatch_get_main_queue()得到主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
注意:主隊列是GCD自帶的一種特殊的串行隊列,放在主隊列中的任務,都會放到主線程中執行。
複製代碼

併發隊列 併發隊列能夠同時調度多個任務,調度任務的方式,取決於執行任務的函數;併發功能只有在異步的(dispatch_async)函數下才有效;異步狀態下,開啓的線程上線由GCD底層決定。

// 1.使用dispatch_queue_create函數建立隊列
dispatch_queue_t
dispatch_queue_create(const char *label, // 隊列名稱,該名稱能夠協助開發調試以及崩潰分析報告 
dispatch_queue_attr_t attr); // 隊列的類型

// 2.建立併發隊列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
複製代碼

自定義隊列在MRC開發時須要使用dispatch_release釋放隊列

#if !__has_feature(objc_arc)
    dispatch_release(queue);
#endif
複製代碼

<2>主隊列 主隊列負責在主線程上調度任務,若是在主線程上有任務執行,會等待主線程空閒後再調度任務執行。 主隊列用於UI以及觸摸事件等的操做,咱們在進行線程間通訊,一般是返回主線程更新UI的時候使用到

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 耗時操做
    // ...
    //放回主線程的函數
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主線程更新 UI
    });
});
複製代碼

<3>全局併發隊列

全局併發隊列是由蘋果API提供的,方便程序員使用多線程。

//使用dispatch_get_global_queue函數得到全局的併發隊列
dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority, unsigned long flags);
// dispatch_queue_priority_t priority(隊列的優先級 )
// unsigned long flags( 此參數暫時無用,用0便可 )

//得到全局併發隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
複製代碼

全局併發隊列有優先級

//全局併發隊列的優先級
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高優先級
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默認(中)優先級
//注意,自定義隊列的優先級都是默認優先級
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低優先級
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 後臺優先級
複製代碼

然而,iOS8 開始使用 QOS(服務質量) 替代了原有的優先級。獲取全局併發隊列時,直接傳遞 0,能夠實現 iOS 7 & iOS 8 later 的適配。

//像這樣
dispatch_get_global_queue(0, 0);
複製代碼

<4>全局併發隊列與併發隊列的區別

全局併發隊列與併發隊列的調度方法相同 全局併發隊列沒有隊列名稱 在MRC開發中,全局併發隊列不須要手動釋放

<5>QOS (服務質量) iOS 8.0 推出

QOS_CLASS_USER_INTERACTIVE:用戶交互,會要求 CPU 儘量地調度此任務,耗時操做不該該使用此服務質量
QOS_CLASS_USER_INITIATED:用戶發起,比 QOS_CLASS_USER_INTERACTIVE 的調度級別低,可是比默認級別高;耗時操做一樣不該該使用此服務質量;若是用戶但願任務儘快執行完畢返回結果,能夠選擇此服務質量;
QOS_CLASS_DEFAULT:默認,此 QOS 不是爲添加任務準備的,主要用於傳送或恢復由系統提供的 QOS 數值時使用
QOS_CLASS_UTILITY:實用,耗時操做可使用此服務質量;
QOS_CLASS_BACKGROUND:後臺,指定任務以最節能的方式運行
QOS_CLASS_UNSPECIFIED:沒有指定 QOS
複製代碼

###3.執行任務的函數 <1>同步(dispatch_sync)

執行完這一句代碼,再執行後續的代碼就是同步

任務被添加到隊列後,會當前線程被調度;隊列中的任務同步執行完成後,纔會調度後續任務。-在主線程中,向主隊列添加同步任務,會形成死鎖 -在其餘線程中,向主隊列向主隊列添加同步任務,則會在主線程中同步執行。 具體是否會形成死鎖,以及死鎖的緣由,還須要針對具體的狀況分析,理解隊列和執行任務的函數纔是關鍵。實際開發中通常只要記住經常使用的組合就能夠了。 咱們能夠利用同步的機制,創建任務之間的依賴關係 例如:

用戶登陸後,纔可以併發下載多部小說 只有「用戶登陸」任務執行完成以後,多個下載小說的任務纔可以「異步」執行 全部下載任務都依賴「用戶登陸」

<2>異步(dispatch_async)

沒必要等待這一句代碼執行完,就執行下一句代碼就是異步

異步是多線程的代名詞,當任務被添加到主隊列後,會等待主線程空閒時纔會調度該任務;添加到其餘線程時,會開啓新的線程調度任務。 <3>以函數指針的方式調度任務 函數指針的調用方式有兩種,一樣是同步和異步;函數指針的傳遞相似於 pthread。

dispatch_sync_f
dispatch_async_f
複製代碼

函數指針調用在實際開發中幾乎不用,只是有些面試中會問到,dispatch + block 纔是 gcd 的主流! ###4.開發中如何選擇隊列 選擇隊列固然是要先了解隊列的特色 串行隊列:對執行效率要求不高,對執行順序要求高,性能消耗小 併發隊列:對執行效率要求高,對執行順序要求不高,性能消耗大 若是不想兼顧 MRC 中隊列的釋放,建議選擇使用全局隊列 + 異步任務。 ##3、GCD的其餘用法 ###1.延時執行 參數1:從如今開始通過多少納秒,參數2:調度任務的隊列,參數3:異步執行的任務 dispatch_after(when, queue, block) 例如:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 2秒後異步執行這裏的代碼...
});
複製代碼

###2.一次性執行 應用場景:保證某段代碼在程序運行過程當中只被執行一次,在單例設計模式中被普遍使用。

// 使用dispatch_once函數能保證某段代碼在程序運行過程當中只被執行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只執行1次的代碼(這裏面默認是線程安全的)
});
複製代碼

###3.調度組(隊列組) 應用場景:須要在多個耗時操做執行完畢以後,再統一作後續處理

//建立調度組
dispatch_group_t group = dispatch_group_create();
//將調度組添加到隊列,執行 block 任務
dispatch_group_async(group, queue, block);
//當調度組中的全部任務執行結束後,得到通知,統一作後續操做
dispatch_group_notify(group, dispatch_get_main_queue(), block);
複製代碼

例如:

// 分別異步執行2個耗時的操做、2個異步操做都執行完畢後,再回到主線程執行操做
dispatch_group_t group =  dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 執行1個耗時的異步操做
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 執行1個耗時的異步操做
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 等前面的異步操做都執行完畢後,回到主線程...
});
複製代碼

##4、單例模式

做用: 能夠保證在程序運行過程,一個類只有一個實例,並且該實例易於供外界訪問。從而方便地控制了實例個數,並節約系統資源 使用場合: 在整個應用程序中,共享一份資源(這份資源只須要建立初始化1次)

實現方法 重寫實現

// 1.在.m中保留一個全局的static的實例
static id _instance;

// 2.重寫allocWithZone:方法,在這裏建立惟一的實例(注意線程安全)
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [super allocWithZone:zone];
    });
    return _instance;
}

// 3.提供1個類方法讓外界訪問惟一的實例
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    });
    return _instance;
}

// 4.實現copyWithZone:方法
- (id)copyWithZone:(struct _NSZone *)zone
{
    return _instance;
}
複製代碼

宏實現

// .h文件
#define SingletonH(name) + (instancetype)shared##name;

// .m文件
#define SingletonM(name) 
static id _instance; 
 
+ (instancetype)allocWithZone:(struct _NSZone *)zone 
{ 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        _instance = [super allocWithZone:zone]; 
    }); 
    return _instance; 
} 
 
+ (instancetype)shared##name 
{ 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        _instance = [[self alloc] init]; 
    }); 
    return _instance; 
} 
 
- (id)copyWithZone:(NSZone *)zone 
{ 
    return _instance; 
}
複製代碼

#第四部分 NSOperation 文章太長了,最後一部分獨立出來吧。 [NSOperation以及多線程技術比較]3

相關文章
相關標籤/搜索