談談iOS面試常提及到的線程間的通信

程序猿思考問題時的樣子

我們看圖片只是樂呵一下,程序猿思考問題差不多就是這個樣子,
今天同事在線程通信這一塊有點疑問,我們下面來分析一下,系統都提供給我們那些,其實我們都知道,但是很少去關注這些API,也正是這些API,來回在APP中去執行各種不同的線程和隊列


一、常見的線程間通信 GCD

我們先來看一個系統的例子:

    //開啟一個全局隊列的子線程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
            //1. 開始請求數據
            //...
            // 2. 數據請求完畢
            //我們知道UI的更新必須在主線程操作,所以我們要從子線程回調到主線程
        dispatch_async(dispatch_get_main_queue(), ^{

                //我已經回到主線程更新
        });

    });

如上所述,我們下面進行一個測試:

     //自定義隊列,開啟新的子線程
    dispatch_queue_t    custom_queue = dispatch_queue_create("concurrentqueue", DISPATCH_QUEUE_CONCURRENT);
    for (int i=0; i<10; i++) {
        dispatch_async(custom_queue, ^{
            NSLog(@"## 并行隊列 %d ##",i);
                //數據更新完畢回調主線程 線程之間的通信
            dispatch_async(dispatch_get_main_queue(), ^{

                NSLog(@"### 我在主線程 通信 ##");

            });


        });
    }

  • 線程中延遲調用某個方法
    //線程延遲調用 通信
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSLog(@"## 在主線程延遲5秒調用 ##");
    });
  • 線程休眠幾秒的方法
    sleep(6); : 這里是休眠6秒

  • 常用的線程通信方法有以下幾種:(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), ^{
           //同步線程操作可以保證數據的安全完整性
    });
    

二、了解一下NSObject中的對象線程訪問模式

  1. 我們介紹簡單的perfermselecter選擇器實現線程通信

        //數據請求完畢回調到主線程,更新UI資源信息  waitUntilDone    設置YES ,代表等待當前線程執行完畢
    [self performSelectorOnMainThread:@selector(dothing:) withObject:@[@"1"] waitUntilDone:YES];
    
  2. 如果需要執行到后臺線程,則直接前往后臺線程執行即可

      //將當前的邏輯轉到后臺線程去執行
    [self performSelectorInBackground:@selector(dothing:) withObject:@[@"2"]];
    
  3. 自己定義線程,將當前數據轉移到指定的線程內去通信操作

     //支持自定義線程通信執行相應的操作
    NSThread * thread = [[NSThread alloc]init];
    [thread start];
        //當我們需要在特定的線程內去執行某一些數據的時候,我們需要指定某一個線程操作
    [self performSelector:@selector(dothing:) onThread:thread withObject:nil waitUntilDone:YES];
    //支持自定義線程通信執行相應的操作
    NSThread * thread = [[NSThread alloc]initWithTarget:self selector:@selector(testThread) object:nil];
    [thread start];
       //當我們需要在特定的線程內去執行某一些數據的時候,我們需要指定某一個線程操作
    [self performSelector:@selector(dothing:) onThread:thread withObject:nil waitUntilDone:YES];

  • 上面幾種方法就是我們常用的對象調用常用的線程間通信,我們可以根據不同的情況,采取不同的線程執行狀態.
增加一個特殊的線程常駐RunLoop 的做法
  • 需求: 我們經常要用到常駐線程來請求數據,但是請求有時候在線程會退出,這個時候我們需要用一下方法:
//有時候需要線程單獨跑一個RunLoop 來保證我們的請求對象存在,不至于會被系統釋放掉

  NSThread * runLoopThread = [[NSThread alloc]initWithTarget:self selector:@selector(entryThreadPoint) object:nil];
  [runLoopThread start];
  [self performSelector:@selector(handleMutiData) onThread:runLoopThread withObject:nil waitUntilDone:YES];


//給線程增加一個run loop 來保持對象的引用
- (void)entryThreadPoint{

  @autoreleasepool {
      [NSThread currentThread].name = @"自定義線程名字";
      NSRunLoop * runloop = [NSRunLoop currentRunLoop];
      [runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
      [runloop run];
  }
}
- (void)handleMutiData{
  NSLog(@"## 我是跑在runloop的線程 ##");
}

最后測試截圖如下,看下我們的線程是不是已經加入runloop 了

這個就是最后的執行了

補充1:
  • 有個地方需要補充以下: 請先看完下面的之后,再回頭看這個地方。
    需求: 如何動態的調整隊列的優先級,執行層級,我們這里多加一個函數
  //  設置目標隊列  queue 既是target , object 是我們將要指定的dispatch 對象, 第一個參數要用自定義的隊列,不能使用系統的全局隊列,系統的隊列層級我們無法修改
void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);

這個就是我們指定一個隊列執行在目標隊列上,和目標隊列一個層級
我們看下代碼如何調用:

 dispatch_queue_t queue1 = dispatch_queue_create("queue1", NULL);
 dispatch_queue_t queue2 = dispatch_queue_create("queue2", NULL);
 dispatch_set_target_queue(queue1, queue2);
 dispatch_async(queue2, ^{
     NSLog(@"## 我是queue2 ----0###");
 });
 dispatch_async(queue1, ^{
     NSLog(@"## 我是queue1 ------1##");
 });
 dispatch_async(queue2, ^{
     NSLog(@"## 我是queue2  ----2 ##");
 });
 dispatch_async(queue1, ^{
     NSLog(@"## 我是queue1 ------3 ##");
 });
 dispatch_async(queue1, ^{
     NSLog(@"## 我是queue1 ------4 ##");
 });
 dispatch_async(queue2, ^{
     NSLog(@"## 我是queue2    ---5 ##");
 });

  • 我們可以先猜想一下輸出結果,會按照我們想要的結果顯示么
    答案:
2016-06-01 11:53:36.333 RunTimeModify[41214:6432344] ## 我是queue2 ----0###
2016-06-01 11:53:36.339 RunTimeModify[41214:6432344] ## 我是queue1 ------1##
2016-06-01 11:53:36.341 RunTimeModify[41214:6432344] ## 我是queue1 ------3 ##
2016-06-01 11:53:36.343 RunTimeModify[41214:6432344] ## 我是queue1 ------4 ##
2016-06-01 11:53:36.344 RunTimeModify[41214:6432344] ## 我是queue2  ----2 ##
2016-06-01 11:53:36.344 RunTimeModify[41214:6432344] ## 我是queue2    ---5 ##

相信大家發現問題了。
我們調整一下Target 的順序:

 dispatch_set_target_queue(queue2, queue1);

我們再來看一下輸出:

2016-06-01 11:57:30.483 RunTimeModify[41297:6435226] ## 我是queue2 ----0###
2016-06-01 11:57:30.486 RunTimeModify[41297:6435226] ## 我是queue2  ----2 ##
2016-06-01 11:57:30.486 RunTimeModify[41297:6435226] ## 我是queue2    ---5 ##
2016-06-01 11:57:30.487 RunTimeModify[41297:6435226] ## 我是queue1 ------1##
2016-06-01 11:57:30.487 RunTimeModify[41297:6435226] ## 我是queue1 ------3 ##
2016-06-01 11:57:30.487 RunTimeModify[41297:6435226] ## 我是queue1 ------4 ##

以上只是簡單的使用,我們不做什么的測試,了解就行:
以上這個地方,大家還會有個小疑問:連接在這里,stack 上找的How does dispatch_set_target_queue work?
大家看到這個或許就明白怎么工作的了

補充2:

謝謝 @激動的馬 問題 ,還要增加一種通信方法

  • NSOperationQueue  系統自帶的更新UI的方法 ,這種我不經常用,推薦GCD
    
    //注意點 : 設計到UI的更新在主隊列
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        NSLog(@"###  我是在主隊列執行的block  ####");
    }];
//這個只是用作判斷當前線程是否是主線程
    if ([[NSThread currentThread] isMainThread]) {
        NSLog(@"## 我是主線程 ##");
    }else{
        NSLog(@"## 我是子線程 ###");
    }
補充三、
  • GCD中阻塞線程之用法
//主要涉及到下面這個API,該API是保證自己的block會等待當前隊列之前的block全部執行完畢,在執行自己的block,最后在執行后面的任務
void
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

我們用代碼簡單的看一下調用順序:

   dispatch_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"## 全局隊列異步執行 第1個任務 ##");
   });
   dispatch_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"## 全局隊列異步執行 第2個任務 ##");
   });
   dispatch_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"## 全局隊列異步執行 第4個任務 ##");
   });
   dispatch_barrier_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"## 異步阻塞當前線程,會等待當前線程執行完畢  ###");
   });
   dispatch_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"### 我在全局隊列線程內執行  第3個任務###");
   });
* 我們看下控制臺輸出:
     2016-06-01 14:04:13.973 RunTimeModify[42377:6489044] ## 全局隊列異步執行 第1個任務 ##
     2016-06-01 14:04:13.973 RunTimeModify[42377:6489051] ## 全局隊列異步執行 第2個任務 ##
     2016-06-01 14:04:13.973 RunTimeModify[42377:6489063] ## 全局隊列異步執行 第4個任務 ##
     2016-06-01 14:04:13.973 RunTimeModify[42377:6489064] ## 異步阻塞當前線程,會等待當前線程執行    完畢  ###
     2016-06-01 14:04:13.973 RunTimeModify[42377:6489065] ### 我在全局隊列線程內執行  第3個任務###

大概可以看一下阻塞這個API是怎么用的,可以在項目總很好的處理我們的問題

  • 我們在繼續看下面的循環執行的block任務:
for (int i=0; i<5; i++) {
        dispatch_barrier_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"## 我是阻塞線程執行順序 %d ###",i);
        });

    }
    for (int i=0; i<5; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"### 我是全部異步隊列任務 : %d  ###",i);
        });
    }

我們看下控制臺打印輸出的日志系統:

2016-06-01 14:07:42.534 RunTimeModify[42437:6491161] ## 我是阻塞線程執行順序 1 ###
2016-06-01 14:07:42.534 RunTimeModify[42437:6491166] ## 我是阻塞線程執行順序 0 ###
2016-06-01 14:07:42.534 RunTimeModify[42437:6491168] ## 我是阻塞線程執行順序 2 ###
2016-06-01 14:07:42.534 RunTimeModify[42437:6491179] ## 我是阻塞線程執行順序 3 ###
2016-06-01 14:07:42.534 RunTimeModify[42437:6491180] ## 我是阻塞線程執行順序 4 ###
2016-06-01 14:07:42.535 RunTimeModify[42437:6491181] ### 我是全部異步隊列任務 : 0  ###
2016-06-01 14:07:42.535 RunTimeModify[42437:6491161] ### 我是全部異步隊列任務 : 1  ###
2016-06-01 14:07:42.536 RunTimeModify[42437:6491166] ### 我是全部異步隊列任務 : 2  ###
2016-06-01 14:07:42.536 RunTimeModify[42437:6491182] ### 我是全部異步隊列任務 : 3  ###
2016-06-01 14:07:42.537 RunTimeModify[42437:6491168] ### 我是全部異步隊列任務 : 4  ###

雖然都是異步任務,但是執行的很有規則。我們可以去嘗試測試一下反過來的結果

  • 可以測試下面這個執行順序:
//測試一下,是不是阻塞API之前的任務必須要執行完畢,看下有什么問題
 for (int i=0; i<5; i++) {

       dispatch_barrier_async(dispatch_get_global_queue(0, 0), ^{
           NSLog(@"## 我是阻塞線程執行順序 %d ###",i);
       });
       dispatch_async(dispatch_get_global_queue(0, 0), ^{
           NSLog(@"### 我是全部異步隊列任務 : %d  ###",i);
       });
   }
  • 經常遇到的問題:
    1. 網絡請求到數據,但是UI沒有及時更新。
    2. 在子線程的block內執行更新UI操作,控制臺一般都會有錯誤線程輸出的
    3. 如果只是邏輯的處理,無關UI,處理數據量較大,可以選擇子線程處理。

本文暫時先介紹到這里,如果還想了解用信號量控制線程可以參考另外一篇博客:關于dispatch 下一遍補充了源的問題
iOS 關于dispatch_semaphore_t 和 dispatch_group_t 的簡單實用,用于多網絡異步回調通知

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 背景 擔心了兩周的我終于輪到去醫院做胃鏡檢查了!去的時候我都想好了最壞的可能(胃癌),之前在網上查的癥狀都很相似。...
    Dely閱讀 9,275評論 21 42
  • 一、前言 本篇博文介紹的是iOS中常用的幾個多線程技術: NSThread GCD NSOperation 由于a...
    和玨貓閱讀 588評論 0 1
  • 文/鹿田小靜 1. 最近喜歡上“性感”這個詞,也喜歡上了性感的狀態,以前喜歡走小清新和文藝范的路線,讀了很多文字以...
    鹿田閱讀 1,122評論 7 12
  • 文 果果果果木 今天剽悍晨讀分享的書《吸引力是這樣煉成的》。文中分別從“立刻吸引陌生人”、“朋友就像保險單”、“備...
    果果果果木閱讀 266評論 2 2
  • The differences betwence Chinese food and Eating Habits a...
    Chineseyoyo閱讀 1,349評論 0 0