iOS知識梳理 - 多線程(2)API數理

在iOS平臺下使用多線程,一般來講有四套方案:

  1. 基於c語言的Pthreads接口,這是POSIX的線程標準,在Linux/unix/windows平臺上都有實現,c語言編程時使用廣泛,但在iOS開發中使用較少。暴露的接口比較底層,功能完善,相應的,就需要程序員管理線程的生命週期,使用相對比較麻煩。
  2. NSThread,objc的線程接口,基本可以理解是Pthreads的面向對象封裝。面向對象後管理起來容易一點,但仍然需要付出一定的手動管理代價。
  3. GCD(Grand Central Dispatch),是一種基於線程池的多任務管理接口。GCD抽象出了隊列和任務的概念,開發者只需要把任務丟到合適的隊列裏,不用關注具體的線程管理,GCD會自動管理線程的生命週期。剝離了線程使用的很多細節,接口方便友好,實際使用比較廣泛。
  4. NSOperation/NSOperationQueue,大體上相當於GCD的Objc封裝,提供了一些在GCD中不容易實現的特性,如:限制最大併發數量,操作之間的依賴關係等。由於使用比GCD更麻煩,因此使用並不特別廣泛。但涉及限制併發數和操作依賴的時候肯定是用NSOperation更好的。

Pthreads

Pthreads是POSIX標準的線程,這套標準規定了一個標準意義上的線程應當具備的完整能力。這裏列舉其主要接口:

  1. 創建

    • int pthread_create (pthread_t *thread,pthread_attr_t *attr,void *(*start_routine)(void *),void *arg)
  2. 結束線程

    • void pthread_exit (void *retval):線程內退出
    • int pthread_cancel (pthread_t thread):從外部終止一個線程
  3. 阻塞

    • int pthread_join(pthread_t thread, void **retval)
    • 阻塞等待另一個線程結束(exit)
    • unsigned sleep(unsigned seconds)
    • 睡一會兒
  4. 分離

    • int pthread_detach(pthread_t tid)
    • 默認地,一個pthread執行完後不會釋放資源,而是保留其執行結束的狀態,detach狀態下則會執行結束立即釋放。也可以在創建的時候帶個參數,創建detach的線程。
    1. 鎖放後面一起對比。
    2. 互斥鎖pthread_mutex_
    3. 自旋鎖pthread_spin_
    4. 讀寫鎖pthread_rwlock_
    5. 條件鎖pthread_con_

鎖放後面一起對比。

參考iOS多線程Pthreads篇

NSThread

線程的Objc封裝,用得不多。現在應該都是基於pthread封裝的。

// 1. 創建線程
- (instancetype)init;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
- (instancetype)initWithBlock:(void (^)(void))block ;

// 創建detach線程,即執行完就會立即釋放
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

// 2. 結束線程
+ (void)exit;//結束當前線程
- (void)start;//結束那個線程

// 3. 阻塞
// NSThread沒有類似join的方法,可以通過鎖來實現。
// 不過它會sleep
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

關於優先級

一開始線程的優先級是通過threadPriority來控制的,是個0~1.0之間double類型的屬性。不過iOS8之後,蘋果推動使用NSQualityOfService來代表優先級,主要原因還是希望開發者忽略底層線程相關的細節,可以看到Qos的描述已經是偏應用上層的劃分方式了:

typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
}

GCD

Grand Central Dispatch (GCD)是 Apple 開發的一個多核編程的解決方法,基本上可以理解爲iOS平臺下的線程池接口。

GCD中有4個關鍵概念:

  • 同步調用:dispatch_sync
  • 異步調用:dispatch_async
  • 串行隊列:DISPATCH_QUEUE_SERIAL
  • 併發隊列:DISPATCH_QUEUE_CONCURRENT

分派一個任務的時候,有同步和異步兩種方式,關注的是當前上下文跟調用的任務之間的關係,同步調用則阻塞等待調用完成後才繼續往下執行,就像同步的網絡請求一樣;異步調用則直接往下執行,dispatch出去的task自己玩去吧。

GCD中,任務是被放到隊列裏然後按一定的規則調度到不同的線程裏執行的。這裏的隊列分爲串行隊列和併發隊列兩種,如果是串行隊列,那麼丟進去的任務是串行執行的,如果是併發隊列,那麼丟進去的任務是併發執行的。

基本使用

GCD爲我們提供了幾個默認隊列:

  1. 主隊列
dispatch_queue_t queue = dispatch_get_main_queue();

主隊列是個串行隊列,和主線程是綁定的。

  1. 全局併發隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

系統提供了四個不同優先級的全局併發隊列,通常我們使用default級別。

我們也可以自己創建隊列:

// 串行隊列的創建方法
dispatch_queue_t queue = dispatch_queue_create("testQueue1", DISPATCH_QUEUE_SERIAL);
// 併發隊列的創建方法
dispatch_queue_t queue = dispatch_queue_create("testQueue2", DISPATCH_QUEUE_CONCURRENT);

使用時,需要在主線程執行的任務丟到main_queue,主要是UI或其它只能在主線程調用的系統方法,這沒什麼問題;

一些有序但不需要放到主線程的,我們可以自己創建串行隊列進行處理。

併發任務,通常dispatch_async到default級別的global_queue裏即可。一般不需要自己創建併發隊列。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
    // do something
});

我們較少使用dispatch_sync這個接口,除非上下文對順序有強依賴並且不得不在另一個隊列執行的邏輯。

例如imageNamed:在iOS9以前不是線程安全的,我們要拋到主線程執行,但下文對其有較強的依賴:

// ...currently in a subthread
__block UIImage *image;
dispatch_sync_on_main_queue(^{
    image = [UIImage imageNamed:@"Resource/img"];
});
attachment.image = image;

但更多的時候我們爲了避免使用dispatch_sync,會使用dispatch_async到目標隊列,執行完再dispatch_async回來。

dispatch_sync的使用一定要慎之又慎。如果在一個串行隊列dispatch到當前隊列,就會造成死鎖。因爲當前任務阻塞住了,等待這個block執行完才繼續執行,但gcd的串行機制,這個block還在排隊,必須等到當前任務執行完纔會開始執行,因此死鎖。如果是併發隊列那麼不會造成死鎖。但如果block裏面的邏輯又涉及其它的鎖機制,這裏的情況可能就會非常複雜。因此dispatch_sync這個東西儘量少用,不得不用時一定要梳理得非常清楚。

GCD的能力很豐富,除了上面的基本用法外,還有很多場景會用到:

定時器

由於NSTimer容易造成循環引用,並且不改RunLoopMode時竟然會被界面滑動給擠掉,子線程不開啓Runloop不能使用,種種限制比較多,有時候我們會用GCD提供的相關能力替代。

一次性定時器

可以用dispatch_after,如下:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)),dispatch_get_main_queue(), ^{
});

gcd提供了時間類型dispatch_time_t,看起來是個Int64好像跟後面的(int64_t)(3 * NSEC_PER_SEC)是一回事一樣,在模擬器上用dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)減去DISPATCH_TIME_NOW,結果正好接近1 * NSEC_PER_SEC,看着跟真的一樣...當時還正好在網上看到有個demo對dispatch_time_t加加減減來算時間...我就信了...實在是太年輕了

看一下官方的解釋

/*!
 * @typedef dispatch_time_t
 *
 * @abstract
 * A somewhat abstract representation of time; where zero means "now" and
 * DISPATCH_TIME_FOREVER means "infinity" and every value in between is an
 * opaque encoding.
 */
typedef uint64_t dispatch_time_t;

dispatch_time_t是個時間的抽象表示,從現在到永遠在uint64上的映射,這個映射還是不透明的,也就是說大概率不是個均勻映射,反正,別指望着隨便用了。

上面提到的1s的例子,只是個巧合,在模擬器上對得上,但是在真機上就不行了。而我當初看到的那個加加減減的demo,其實是swift寫的,在swift下對應的類型重載了運算符所以可以直接用+/-運算。

總之,dispatch_time_t只能結合gcd的接口使用,它的值對應着一個時間但跟我們理解的時分秒這種時間單位完全沒有關係。我們算時間還是老老實實用NSTimerInterval。

循環的定時器

GCD提供了一種稱爲Source的機制,將source和一個任務關聯,可以通過觸發這個source的方式執行關聯的任務。timer就是其中的一種source。其它的source實際使用非常少。

使用方式如下

_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 1*NSEC_PER_SEC), 1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
    // do something
});
dispatch_resume(_timer);

Dispatch Group

dispatch_group可以把多個任務合成一組,於是可以知道一組任務何時全部執行完。

NSLog(@"--- 開始設置任務 ----");
// 因爲 dispatch_group_wait 會阻塞線程,所以創建一個新的線程,用來完成任務
// 同時用異步的方式向新線程(tasksQueue)中添加任務
dispatch_queue_t tasksQueue = dispatch_queue_create("tasksQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(tasksQueue, ^{
    // 真正用來完成任務的線程
    dispatch_queue_t performTasksQueue = dispatch_queue_create("performTasksQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    for (int i = 0; i < 3; i++) {
        // 入組之後的 block 會被 group 監聽
        // 注意:dispatch_group_enter 一定和 dispatch_group_leave 要配對出現
        dispatch_group_enter(group);
        dispatch_async(performTasksQueue, ^{
            NSLog(@"開始第 %zd 項任務", i);
            [NSThread sleepForTimeInterval:(3 - i)];
            dispatch_group_leave(group);
            NSLog(@"完成第 %zd 項任務", i);
        });
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"全部任務完成");
    });
});
NSLog(@"--- 結束設置任務 ----");

dispatch_once

結合dispatch_once_t,保證只執行一次,由於語義比較清晰,現在是實習單例的最佳寫法。

+ (SomeManager *)sharedInstance
{
    static SomeManager *manager = nil;
    static dispatch_once_t token;

    dispatch_once(&token, ^{
        manager = [[SomeManager alloc] init];
    });
    return manager;
}

dispatch_barrier_async

柵欄函數,只有配合併發隊列纔有意義。當前面的任務都執行完後,當前任務纔會開始執行,當前任務執行完後,後面的任務纔會開始執行。

- (void)barrier
{
  //同dispatch_queue_create函數生成的concurrent Dispatch Queue隊列一起使用
    dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        NSLog(@"----1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----2-----%@", [NSThread currentThread]);
    });
    
    dispatch_barrier_async(queue, ^{
        NSLog(@"----barrier-----%@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"----3-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----4-----%@", [NSThread currentThread]);
    });
}

NSOperationQueue

NSOperationQueue/NSOperation本身是早於GCD推出的,不過後來基於GCD重寫了,可以理解爲基於GCD的面向對象封裝,類似的,也沿用了任務/隊列的概念。

NSOperationQueue/NSOperation對比GCD的優點:

  1. 可添加完成的代碼塊,在操作完成後執行。
  2. 添加操作之間的依賴關係,方便的控制執行順序。
  3. 設定操作執行的優先級。
  4. 可以很方便的取消一個操作的執行。
  5. 使用 KVO 觀察對操作執行狀態的更改:isExecuteing、isFinished、isCancelled。

基本使用如下:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
  for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印當前線程
  }
}];
[queue addOperation:operation];

可以看出,比GCD用起來麻煩了很多,所以通常只有涉及到上面的幾點優勢場景纔會用。

逐個來看一下。

  1. 可添加完成的代碼塊,在操作完成後執行。

    • NSOperation的方法
    • - (void)setCompletionBlock:(void (^)(void))block; completionBlock 會在當前操作執行完畢時執行 completionBlock。`
  2. 添加操作之間的依賴關係,方便的控制執行順序。

    • 仍是NSOperation的方法
    • - (void)addDependency:(NSOperation *)op;
    • - (void)removeDependency:(NSOperation *)op;
  3. 設定操作執行的優先級。

    • NSOpetation的屬性@property NSOperationQueuePriority queuePriority;
    • 優先看依賴關係,多個task都ready時才按優先級順序
  4. 可以很方便的取消一個操作的執行。

    • NSOperation的方法- (void)cancel;
  5. 使用 KVO 觀察對操作執行狀態的更改:isExecuteing、isFinished、isCancelled。、

    @property (readonly, getter=isCancelled) BOOL cancelled;
    @property (readonly, getter=isExecuting) BOOL executing;
    @property (readonly, getter=isFinished) BOOL finished;
    @property (readonly, getter=isReady) BOOL ready;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章