iOS 多執行緒總結

orilme發表於2019-03-25
更多實用詳見 Demo
本文導讀

一些相關總結(同步、非同步、並行、序列概念,GCD、NSOperation對比)
一、程式與執行緒概念
二、多執行緒-GCD
三、多執行緒-NSOperation
四、多執行緒-NSThread
五、佇列概念
六、多執行緒面試題

同步、非同步、並行、序列
  1. 同步和非同步決定了要不要開啟行的新的執行緒。同步在當前執行緒中執行任務,不具備開啟新執行緒的能力。非同步在新的執行緒中執行任務,具備開啟新執行緒的能力。
  2. 並行和序列決定了任務的執行方式。併發多個任務併發(同時)執行。序列,一個任務執行完畢後,再執行下一個任務。
GCD、NSOperation對比:
  • GCD --> iOS 4.0
    (1)將任務(block)新增到佇列(序列/併發(全域性)),指定執行任務的方法(同步(阻塞)/非同步)
    (2)拿到 dispatch_get_main_queue()。 執行緒間通訊
    (3)NSOperation無法做到一次性執行、延遲執行、排程組(op相對複雜)
    (4)提供了更多的控制能力以及操作佇列中所不能使用的底層函式

  • NSOperation ----> iOS 2.0 (後來蘋果改造了NSOperation的底層)
    (1)將操作(非同步執行)新增到佇列(併發/全域性)
    (2)[NSOperationQueue mainQueue] 主佇列。 任務新增到主佇列, 就會在主執行緒執行
    (3)提供了一些GCD不好實現的,”最大併發數“、依賴關係、暫停/繼續 --- 掛起、取消所有的任務

  • 對比:
    (1)GCD是純C語言的API,NSOperationQueue是基於GCD的OC版本封裝
    (2)GCD只支援FIFO的佇列,NSOperationQueue可以很方便的調整執行順序、設定最大併發數量 (FIFO 就是先進先出)
    (3)NSOperationQueue可以很輕鬆的在Operation間設定依賴關係,而GCD需要寫很多程式碼才能實現
    (4)NSOperationQueue支援KVO(鍵值觀察者),可以監聽operation是否正在執行 (isExecuted)、是否結束(isFinished)、是否取消(isCanceld)
    (5)GCD的執行速度比NSOperationQueue快(封裝GCD,更高層的東西,效能不好,因為還要轉化成GCD)快

  • 在專案什麼時候選擇使用GCD,什麼時候選擇NSOperation?
    · NSOperation:
    任務之間有依賴\或者要監聽任務的執行情況
    專案中使用的優點是它是對執行緒的高度抽象,會使程式結構更好,子類化NSOperation的設計思路具有物件導向的優點(複用、封裝),使得實現多執行緒支援,而介面簡單,建議在複雜專案中使用。
    · GCD:
    任務之間不太依賴 專案中使用GCD的優點是GCD本身非常簡單、易用,對於不復雜的多執行緒操作,會節省程式碼量,而Block引數的使用,會是程式碼更為易讀,建議在簡單專案中使用。

一、程式與執行緒概念:

  • 程式百度百科

    程式(Process)是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。在早期面向程式設計的計算機結構中,程式是程式的基本執行實體;在當代面向執行緒設計的計算機結構中,程式是執行緒的容器。程式是指令、資料及其組織形式的描述,程式是程式的實體。

  • 執行緒百度百科

    執行緒,有時被稱為輕量程式(Lightweight Process,LWP),是程式執行流的最小單元。一個標準的執行緒由執行緒ID,當前指令指標(PC),暫存器集合和堆疊組成。另外,執行緒是程式中的一個實體,是被系統獨立排程和分派的基本單位,執行緒自己不擁有系統資源,只擁有一點兒在執行中必不可少的資源,但它可與同屬一個程式的其它執行緒共享程式所擁有的全部資源。一個執行緒可以建立和撤消另一個執行緒,同一程式中的多個執行緒之間可以併發執行。由於執行緒之間的相互制約,致使執行緒在執行中呈現出間斷性。執行緒也有就緒阻塞執行三種基本狀態。就緒狀態是指執行緒具備執行的所有條件,邏輯上可以執行,在等待處理機;執行狀態是指執行緒佔有處理機正在執行;阻塞狀態是指執行緒在等待一個事件(如某個訊號量),邏輯上不可執行。每一個程式都至少有一個執行緒,若程式只有一個執行緒,那就是程式本身。 執行緒是程式中一個單一的順序控制流程。程式內有一個相對獨立的、可排程的執行單元,是系統獨立排程和分派CPU的基本單位指令執行時的程式的排程單位。在單個程式中同時執行多個執行緒完成不同的工作,稱為多執行緒

  • 程式總結

    1. 程式是指在系統中正在執行的一個應用程式
    2. 每個程式之間是獨立的,每個程式均執行在其專用且受保護的記憶體空間內
  • 執行緒總結

    1. 1個程式要想執行任務,必須得有執行緒(每1個程式至少要有一個執行緒
    2. 執行緒是程式的基本執行單元,一個程式的所有任務都線上程中執行

    程式(process)是一塊包含了某些資源的記憶體區域。作業系統利用程式把它的工作劃分為一些功能單元。程式中所包含的一個或多個執行單元稱為執行緒(thread)。程式還擁有一個私有的虛擬地址空間,該空間僅能被它所包含的執行緒訪問。通常在一個程式中可以包含若干個執行緒,它們可以利用程式所擁有的資源。在引入執行緒的作業系統中,通常都是把程式作為分配資源的基本單位,而把執行緒作為獨立執行和獨立排程的基本單位。由於執行緒比程式更小,基本上不擁有系統資源,故對它的排程所付出的開銷就會小得多,能更高效的提高系統內多個程式間併發執行的程度。簡而言之,一個程式至少有一個程式,一個程式至少有一個執行緒。一個程式就是一個程式,而一個程式中的多個任務則被稱為執行緒。 執行緒只能歸屬於一個程式並且它只能訪問該程式所擁有的資源。當作業系統建立一個程式後,該程式會自動申請一個名為主執行緒或首要執行緒的執行緒。應用程式(application)是由一個或多個相互協作的程式組成的。另外,程式在執行過程中擁有獨立的記憶體單元,而多個執行緒共享記憶體,從而極大地提高了程式的執行效率。執行緒在執行過程中與程式還是有區別的。每個獨立的執行緒有一個程式執行的入口、順序執行序列和程式的出口。但是執行緒不能夠獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制。 從邏輯角度來看,多執行緒的意義在於一個應用程式中,有多個執行部分可以同時執行。但作業系統並沒有將多個執行緒看做多個獨立的應用,來實現程式的排程和管理以及資源分配。這就是程式和執行緒的重要區別。 程式是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,程式是系統進行資源分配和排程的一個獨立單位。 執行緒是程式的一個實體,是CPU排程和分派的基本單位,它是比程式更小的能獨立執行的基本單位。執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程式的其他的執行緒共享程式所擁有的全部資源。

  • 執行緒與程式對比?

    1. 排程:執行緒作為排程和分配的基本單位,程式作為擁有資源的基本單位
    2. 併發性:不僅程式之間可以併發執行,同一個程式的多個執行緒之間也可併發執行
    3. 擁有資源:程式是擁有資源的一個獨立單位,執行緒不擁有系統資源,但可以訪問隸屬於程式的資源
    4. 系統開銷: 程式切換(建立或撤消)時,由於系統都要為之分配和回收資源,導致系統的開銷明顯大於建立或撤消執行緒時的開銷,效率要差一些。對於一些要求同時進行並且又要共享某些變數的併發操作,只能用執行緒,不能用程式
    5. 程式都有自己的獨立地址空間。一個程式崩潰後,在保護模式下不會對其它程式產生影響。執行緒有自己的堆、區域性變數,但執行緒之間沒有單獨的地址空間,一個執行緒死掉就等於整個程式死掉,所以多程式的程式要比多執行緒的程式健壯
    6. 執行緒是指程式內的一個執行單元,也是程式內的可排程實體。它可以與同程式的其他執行緒共享資料,但擁有自己的棧空間

    簡而言之,一個程式至少有一個程式,一個程式至少有一個執行緒(即主執行緒)。一個程式就是一個程式,而一個程式中的多個任務則被稱為執行緒。

  • 學習多執行緒,先了解手機裡有幾個重要的晶片:
    主晶片:CPU(雙核)+GPU(相當於電腦裡的顯示卡)
    記憶體(RAM):相當於電腦的記憶體條
    快閃記憶體晶片:相於於電腦的硬碟
    電源管理晶片
    藍芽、wifi、gps晶片

  • 多執行緒應用:
    耗時操作(資料庫中的讀取,圖片的處理(濾鏡) )
    程式是來幫你分配記憶體的
    多執行緒開執行緒一般五條以內

二、多執行緒-GCD

GCD簡介

全稱Grand Central Dispatch,純C語言,提供了非常多強大的函式

  • 優勢:

    1. 蘋果公司為多核的並行運算提出的解決方案
    2. GCD會自動利用更多的CPU核心(比如雙核、四核)
    3. GCD會自動管理執行緒的生命週期(建立執行緒、排程任務、銷燬執行緒)
    4. 程式設計師只需要告訴GCD想要執行什麼任務,不需要編寫任何執行緒管理程式碼
  • GCD中2個核心概念:

    1. 任務:執行什麼操作
    2. 佇列:用來存放任務
  • GCD的使用就兩個步驟:

    1. 定製任務:確定想要做的事
    2. 將任務新增到佇列中:GCD會自動將佇列中的任務取出,放到對應的執行緒中執行。任務的取出遵循佇列的FIFO原則(先進先出,後進後出)
  • 佇列的型別: GCD的佇列可以分為兩種型別:
    · 併發佇列
    (1) 佇列中的任務 通常 會併發執行
    (2) 可以讓多個任務併發(同時)執行(自動開啟多個執行緒同時執行任務)
    (3) 併發功能只有在非同步(dispatch_async)函式下才有效
    · 序列佇列:
    (1) 佇列中的任務 會順序執行
    (2) 讓任務一個接一個的執行(一個任務執行完畢後,再執行下一個任務)

  • GCD內部是怎麼實現的? (1) iOS和OS X的核心是XNU核心,GCD是基於XNU核心實現的
    (2) GCD的API全部在libdispatch庫中
    (3) GCD底層實現主要有Dispatch Queue和Dispatch Source · Dispatch Queue: 管理block(操作) · Dispatch Source :處理事件(MACH埠傳送,MACH埠接收,檢測與程式相關事件等10種事件)

基本使用
  1. 全域性佇列與並行佇列的區別
    (1) 不需要建立,直接GET就能用
    (2) 兩個佇列的執行效果相同
    (3) 全域性佇列沒有名稱,除錯時,無法確認準確佇列
    (4) 全域性佇列有高中預設優先順序
  2. 全域性佇列 dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    3. 並行佇列 dispatch_queue_t q = dispatch_queue_create("queuename", DISPATCH_QUEUE_CONCURRENT);
    4. 序列佇列 dispatch_queue_t t = dispatch_queue_create("queuename",DISPATCH_QUEUE_SERIAL);
GCD高階:排程組dispatch_group
  • 應用場景: 開發的時候,有的時候出現多個網路請求都完成以後(每一個網路請求的事件長短不一定),再統一通知使用者。比如: 下載小說:三國演義, 紅樓夢, 水滸
      // 例項化一個排程組
      dispatch_group_t group = dispatch_group_create();
      
      // 佇列
      dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
      
      // 任務新增到佇列queue
      dispatch_group_async(group, queue, ^{
          NSLog(@"下載小說A---%@", [NSThread currentThread]);
      });
      
      dispatch_group_async(group, queue, ^{
          NSLog(@"下載小說B---%@", [NSThread currentThread]);
      });
      
      dispatch_group_async(group, queue, ^{
          NSLog(@"下載小說X---%@", [NSThread currentThread]);
      });
      
      // 獲得所有排程組裡面的非同步任務完成的通知
      //    dispatch_group_notify(group, queue, ^{
      //        NSLog(@"下載完成,請觀看%@", [NSThread currentThread]); // 非同步的
      //    });
      
      //注意點: 在排程組完成通知裡,可以跨佇列通訊
      dispatch_group_notify(group, dispatch_get_main_queue(), ^{
          // 更新UI,在主執行緒
          NSLog(@"下載完成,請觀看%@", [NSThread currentThread]); // 非同步的
      });
    複製程式碼
GCD高階:柵欄函式dispatch_barrier_(a)sync

首先是 Apple Documentation 對dispatch_barrier_async的解釋

  • dispatch barrier允許開發者在一個並行佇列中創造一個同步點,其實也是用於在競爭條件下資源的保護,防止同一資源同時被使用。 在並行非同步任務中: barrier_sync前的任務併發執行,syncbarrier後的任務必須等syncbarrier中的任務執行完成之後才會執行他們,也會阻塞主執行緒的任務 barrier_async前的任務併發執行,barrier_async後的任務必須等barrier_async中的任務執行完成之後才會執行他們,但是Barrier不能阻塞主執行緒的任務

  • 作用如:打造執行緒安全的 NSMutableArray NSMutableArray本身是執行緒不安全的。簡單來說,執行緒安全就是多個執行緒訪問同一段程式碼,程式不會異常、不Crash。而編寫執行緒安全的程式碼主要依靠執行緒同步。 1、不使用atomic修飾屬性打造執行緒安全的原因: 1 ) atomic 的記憶體管理語義是原子性的,僅保證了屬性的setter和getter方法是原子性的,是執行緒安全的,但是屬性的其他方法,如陣列新增/移除元素等並不是原子操作,所以不能保證屬性是執行緒安全的。 2 ) atomic雖然保證了getter、setter方法執行緒安全,但是付出的代價很大,執行效率要比nonatomic慢很多倍(有說法是慢10-20倍)。 總之:使用nonatomic修飾NSMutableArray物件就可以了,而使用鎖、dispatch_queue來保證NSMutableArray物件的執行緒安全。 2、打造執行緒安全的NSMutableArray 在《Effective Objective-C 2.0..》書中第41條:多用派發佇列,少用同步鎖中指出:使用“序列同步佇列”(serial synchronization queue),將讀取操作及寫入操作都安排在同一個佇列裡,即可保證資料同步。而通過併發佇列,結合GCD的柵欄塊(barrier)來不僅實現資料同步執行緒安全,還比序列同步佇列方式更高效。

幾種任務巢狀:
  • 在主佇列開啟非同步任務,不會開啟新的執行緒而是依然在主執行緒中執行程式碼塊中的程式碼。為什麼不會阻塞執行緒?  主佇列開啟非同步任務,雖然不會開啟新的執行緒,但是他會把非同步任務降低優先順序,等閒著的時候,就會在主執行緒上執行非同步任務。

  • 在主佇列開啟同步任務,為什麼會阻塞執行緒?  在主佇列開啟同步任務,因為主佇列是序列佇列,裡面的執行緒是有順序的,先執行完一個執行緒才執行下一個執行緒,而主佇列始終就只有一個主執行緒,主執行緒是不會執行完畢的,因為他是無限迴圈的,除非關閉應用程式。因此在主執行緒開啟一個同步任務,同步任務會想搶佔執行的資源,而主執行緒任務一直在執行某些操作,不肯放手。兩個的優先順序都很高,最終導致死鎖,阻塞執行緒了。

  • 主執行緒佇列注意: 下面程式碼執行順序 1111 2222

- (void)main_queue_deadlock {
    dispatch_queue_t q = dispatch_get_main_queue();
    NSLog(@"1111");
    dispatch_async(q, ^{
        NSLog(@"主佇列非同步 %@", [NSThread currentThread]);
    });
    NSLog(@"2222");
    // 下面會造成執行緒死鎖
//    dispatch_sync(q, ^{
//        NSLog(@"主佇列同步 %@", [NSThread currentThread]);
//    });
}
複製程式碼
  • 序列佇列開啟非同步任務後巢狀同步任務造成死鎖
dispatch_queue_t q = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(q, ^{
    NSLog(@"非同步任務 %@", [NSThread currentThread]);
    // 下面開啟同步造成死鎖:因為序列佇列中執行緒是有執行順序的,需要等上面開啟的非同步任務執行完畢,才會執行下面開啟的同步任務。而上面的非同步任務還沒執行完,要到下面的大括號才算執行完畢,而下面的同步任務已經在搶佔資源了,就會發生死鎖。
    dispatch_sync(q, ^{
        NSLog(@"同步任務 %@", [NSThread currentThread]);
    });
});
複製程式碼
  • 序列佇列開啟同步任務後巢狀同步任務造成死鎖
dispatch_queue_t q = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(q, ^{
  NSLog(@"同步任務 %@", [NSThread currentThread]);
  // 下面開啟同步造成死鎖:因為序列佇列中執行緒是有執行順序的,需要等上面開啟的同步任務執行完畢,才會執行下面開啟的同步任務。而上面的同步任務還沒執行完,要到下面的大括號才算執行完畢,而下面的同步任務已經在搶佔資源了,就會發生死鎖。
  dispatch_sync(q, ^{
	 NSLog(@"同步任務 %@", [NSThread currentThread]);
  });
});
NSLog(@"同步任務 %@", [NSThread currentThread]);
複製程式碼
  • 序列佇列開啟同步任務後巢狀非同步任務不造成死鎖
  • 並行佇列的任務巢狀例子
    dispatch_queue_t q = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    // 任務巢狀
    dispatch_sync(q, ^{
        NSLog(@"1 %@", [NSThread currentThread]);
        dispatch_sync(q, ^{
            NSLog(@"2 %@", [NSThread currentThread]);
            dispatch_sync(q, ^{
                NSLog(@"3 %@", [NSThread currentThread]);
            });
        });
        dispatch_async(q, ^{
            NSLog(@"4 %@", [NSThread currentThread]);
        });
        NSLog(@"5 %@", [NSThread currentThread]);
    });
// 執行結果是: 12345 或12354  
複製程式碼

並行佇列裡開啟同步任務是有執行順序的,只有非同步才沒有順序
序列佇列開啟非同步任務,是有順序的

三、多執行緒-NSOperation:

  • 新增依賴
    // 建立佇列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    // 建立3個操作
    NSOperation *a = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operationA---");
    }];
    NSOperation *b = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operationB---");
    }];
    NSOperation *c = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operationC---");
    }];
    
    // 新增依賴
    [c addDependency:a];
    [c addDependency:b];
    
    // 執行操作
    [queue addOperation:a];
    [queue addOperation:b];
    [queue addOperation:c];
複製程式碼
NSOperation queue

存放NSOperation的集合類。
(1) 用來存放NSOperation物件的佇列,可以用來非同步執行一些操作
(2) 一般可以用在網路請求等耗時操作
解釋:操作和操作佇列,基本可以看成java中的執行緒和執行緒池的概念。用於處理ios多執行緒開發的問題。網上部分資料提到一點是,雖然是queue,但是卻並不是帶有佇列的概念,放入的操作並非是按照嚴格的先進現出。
這邊又有個疑點是,對於佇列來說,先進先出的概念是Afunc新增進佇列,Bfunc緊跟著也進入佇列,Afunc先執行這個是必然的,但是Bfunc是等Afunc完全操作完以後,B才開始啟動並且執行,因此佇列的概念離亂上有點違背了多執行緒處理這個概念。但是轉念一想其實可以參考銀行的取票和叫號系統。因此對於A比B先排隊取票但是B率先執行完操作,我們亦然可以感性認為這還是一個佇列。但是後來看到一票關於這操作佇列話題的文章,其中有一句提到“因為兩個操作提交的時間間隔很近,執行緒池中的執行緒,誰先啟動是不定的。”瞬間覺得這個queue名字有點忽悠人了,還不如pool~綜合一點,我們知道他可以比較大的用處在於可以幫組多執行緒程式設計就好了。

四、多執行緒-NSThread

  • 建立執行緒的方式
// 建立執行緒的方式1
- (void)createThread1 {
    // 建立執行緒
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(download:) object:@"http://a.png"];
    thread.name = @"下載執行緒";
    
    // 啟動執行緒(呼叫self的download方法)
    [thread start];
}
// 建立執行緒的方式2
- (void)createThread2 {
    [NSThread detachNewThreadSelector:@selector(download:) toTarget:self withObject:@"http://b.jpg"];
}
// 建立執行緒的方式3
- (void)createThread3 {
    // 這2個不會建立執行緒,在當前執行緒中執行
    //    [self performSelector:@selector(download:) withObject:@"http://c.gif"];
    //    [self download:@"http://c.gif"];
    
    [self performSelectorInBackground:@selector(download:) withObject:@"http://c.gif"];
}
複製程式碼
  • 執行緒安全
-(void)threadSafe {
    self.leftTicketCount = 50;
    
    self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread1.name = @"1號視窗";
    
    self.thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread2.name = @"2號視窗";
    
    self.thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread3.name = @"3號視窗";
    
    [self threadSafeStart];
}
- (void)threadSafeStart {
    [self.thread1 start];
    [self.thread2 start];
    [self.thread3 start];
}
// 賣票
- (void)saleTicket {
    while (1) {
        // ()小括號裡面放的是鎖物件
        @synchronized(self) { // 開始加鎖
            int count = self.leftTicketCount;
            if (count > 0) {
                [NSThread sleepForTimeInterval:0.05];
                
                self.leftTicketCount = count - 1;
                
                NSLog(@"%@賣了一張票, 剩餘%d張票", [NSThread currentThread].name, self.leftTicketCount);
            } else {
                return; // 退出迴圈
            }
        } // 解鎖
    }
}
複製程式碼

五、佇列概念:

  • 主執行緒 每一個應用程式都只有一個主執行緒。
    所有UI的更新工作,都必須在主執行緒上執行! 主執行緒是一直工作的,而且除非將程式殺掉,否則主執行緒的工作永遠不會結束!

  • 佇列和執行緒的區別:
    佇列是管理執行緒的,相當於執行緒池,能管理執行緒什麼時候執行。

  • 佇列分為序列佇列和並行佇列:
    序列佇列:佇列中的執行緒按順序執行(不會同時執行)
    並行佇列:佇列中的執行緒會併發執行,可能會有一個疑問,佇列不是先進先出嗎,如果後面的任務執行完了,怎麼出去的了。這裡需要強調下,任務執行完畢了,不一定出佇列。只有前面的任務執行完了,才會出佇列,也就是說你即使執行完畢了,也必須等前面的任務執行完畢出佇列,才可以出去。

  • 主執行緒和工作執行緒的區別:
    主執行緒: 系統自動建立 能直操作UI 不能做耗時比較多的操作 自帶自動釋放池 RunLoop自動執行
    工作執行緒:程式猿手工建立 不能直接操作UI 寫死迴圈都行 需要手工寫自動釋放池 手動執行

  • 主執行緒佇列和GCD建立的佇列區別: 主執行緒佇列:主執行緒佇列中不能開啟同步,會阻塞主執行緒。只能開啟非同步任務,開啟非同步任務也不會開啟新的執行緒,只是降低非同步任務的優先順序,讓cpu空閒的時候才去呼叫。而同步任務,會搶佔主執行緒的資源,會造成死鎖。 GCD建立的佇列:在GCD中建立的佇列優先順序沒有主佇列高,所以在GCD中的序列佇列開啟同步任務裡面沒有巢狀任務是不會阻塞主執行緒,只有一種可能導致死鎖,就是序列佇列裡,巢狀開啟任務,有可能會導致死鎖。

  • 同步與非同步的區別:  同步任務優先順序高,線上程中有執行順序,不會開啟新的執行緒。 非同步任務優先順序低,線上程中執行沒有順序,看cpu閒不閒。在主佇列中不會開啟新的執行緒,其他佇列會開啟新的執行緒。

六、多執行緒面試題:

  • 回到主執行緒
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 下載圖片
    UIImage *image = nil;
    dispatch_async(dispatch_get_main_queue(), ^{
        // 回到主執行緒
    });
    // [self performSelector:@selector(settingImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES modes:nil];
    // [self performSelectorOnMainThread:@selector(settingImage:) withObject:image waitUntilDone:YES];
});
複製程式碼
  • 子執行緒的定時器: 程式碼片段原有的目的是非同步延遲0.3秒後輸出Hello world。但是執行之後發現不會輸出Hello world。 原因是非主執行緒的NSRunLoop預設沒有開啟,而- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay; 函式內部是通過NSTimer定時器實現,在NSRunLoop沒有開啟的情況下,NSTimer不會得到正常執行。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
     [self performSelector:@selector(fireBlock:) withObject:^{  
        NSLog(@"hello world");  
     } afterDelay:0.3];  
 });  
複製程式碼
  • 主執行緒中執行程式碼
[self performSelectorOnMainThread: withObject: waitUntilDone:];
[self performSelector: onThread:[NSThread mainThread] withObject: waitUntilDone:];
dispatch_async(dispatch_get_main_queue(), ^{
});
複製程式碼
  • 延時執行
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW,  (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){        
});
[self performSelector: withObject: afterDelay:];
[NSTimer scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:];
複製程式碼
  • GCD怎麼用的? 全域性佇列非同步操作,會新建多個子執行緒,操作無序執行,如果佇列前有其他任務,會等待其他任務執行完畢在呼叫; 全域性佇列同步操作,不會新建執行緒,順序執行; 主佇列所有的操作都是主執行緒順序執行,沒有非同步概念,主佇列新增的同步操作永遠不會執行,會死鎖; 序列佇列新增的同步操作會死鎖,但是會執行巢狀同步操作之前的程式碼; 並行佇列新增的同步操作不會死鎖都在主執行緒執行; 全域性佇列新增的同步操作不會死鎖; 同步操作 最主要的目的,阻塞並行佇列任務的執行,只有當前的同步任務執行完畢之後,後邊的任務才會執行,應用:使用者登入。

  • 多執行緒安全怎麼控制:

    1. 只在主執行緒重新整理訪問UI
    2. 如果要防止資源搶奪,得用synchronized進行加鎖保護
    3. 如果非同步操作要保證執行緒安全等問題,儘量使用GCD(有些函式預設就是安全的)
  • @synchronized @synchronized 的作用是建立一個互斥鎖,保證此時沒有其它執行緒對self物件進行修改。這個是objective-c的一個鎖定令牌,防止self物件在同一時間內被其它執行緒訪問,起到執行緒的保護作用。 一般在公用變數的時候使用,如單例模式或者操作類的static變數中使用。

  • NSLock 從微觀上看一個執行緒在CPU上走走停停的,其執行狀態可以分為(經典)三種:執行狀態、就緒狀態、阻塞。 兩個執行緒同時訪問一個共享的變數,一個對其加1、一個對其減1 由於記憶體的速度遠低於CPU的速度,所以在設計CPU時通常不允許直接對記憶體中的資料進行執行。如果要運算記憶體中的資料通常是用一條CPU指令把記憶體中的資料讀入CPU、再用另外一CPU指令對CPU中的資料做運算、最後再用一條CPU指令把結果寫回記憶體。 i++ 讀資料 運算 寫資料 由於CPU在執行兩條指令中間可以被打斷,就可能導致另一個訪問同樣記憶體的執行緒執行,最終導致運算結果出錯 解決辦法就是加執行緒鎖 NSLock lock方法 加鎖:如果這個沒被鎖上則直接上鎖、如果鎖已經被其它執行緒鎖上了當前執行緒就阻塞直至這個鎖被其它執行緒解鎖 unlock方法 解鎖:解開這個鎖,如果有其它執行緒因為等這個鎖而進入了阻塞狀態還要把那個執行緒變成就緒 用lock保護共享資料的原理 先上鎖 訪問共享的資料(臨界區) 解鎖

  • NSCondition 執行緒的同步,有一個經典的生產者消費者問題: 比如一個執行緒A負責下載資料 另一個執行緒B負責處理資料 還有一個執行緒C負責顯示 解決方法就是用NSCondition: lock wait signal unlock 產生者執行緒(產生資料的執行緒),生產資料之後: lock signal(發出訊號:如果有其它執行緒在等待這個訊號就把那個執行緒變為就緒狀態) unlock 消費者執行緒(處理或使用資料的執行緒): lock wait(等待訊號:如果有執行緒發訊號當前執行緒就會進入阻塞狀態、直到有執行緒發出訊號) unlock NSCondition有一個問題:假喚醒(wait的時候明明沒有執行緒發訊號,wait也可能返回),通常用一個表示狀態的變數就能解決這個問題

  • 不可改變的物件,通常是執行緒安全的 執行緒安全的類和函式: NSArray, NSData, NSNumber..... 非執行緒安全: NSBundle, NSCoder, NSArchiver, NSMutableArray

  • 列舉幾種程式的同步機制,並比較其優缺點。 答案:原子操作、訊號量機制、自旋鎖、管程、會合、分散式系統

  • 程式之間通訊的途徑 答案:共享儲存系統訊息傳遞系統管道:以檔案系統為基礎

  • 程式死鎖的原因 答案:資源競爭及程式推進順序非法

  • 死鎖的4個必要條件 答案:互斥、請求保持、不可剝奪、環路

  • 死鎖的處理 答案:鴕鳥策略、預防策略、避免策略、檢測與解除死鎖

相關文章