IOS多線程-使用GCD

GCD全稱 Grand Central Dispatch,是Apple開發的一個多核編程解決辦法。該方法在Mac OSX 10.6 雪豹 中首次推出,隨後引入到IOS4.0中。GCD是一個替代NSThreadNSOperationQueue等技術的方案。php

GCD初探

// 1.隊列
dispatch_queue_t queue = dispatch_queue_create("Typeco", DISPATCH_QUEUE_CONCURRENT);
// 2.任務:無參數,無返回值
void(^block)(void) = ^{
     NSLog(@"這是要執行的任務");
};
// 3.函數
dispatch_async(queue, block);
複製代碼

這裏爲了區分開任務和函數,我把block單獨拆出來,這樣對GCD的認識會更簡單直觀,總結一下就是:git

GCD 就是將指定的任務(block)添加到指定的隊列(queue),而後指定 dispatch 函數執行。任務的執行遵循隊列的FIFO原則:先進先出。github

隊列

  • 串行隊列(Serial)web

    隊列裏的任務一個一個執行,一個任務執行完畢,才執行下一個任務。自定義串行隊列以下:編程

    dispatch_queue_t queue = dispatch_queue_create("Typeco", DISPATCH_QUEUE_SERIAL);
    複製代碼

    其中第一個參數label是用來標識queue的字符串,第二個參數標識隊列的類型,這裏咱們要建立串行隊列:DISPATCH_QUEUE_SERIALapi

    #define DISPATCH_QUEUE_SERIAL NULL安全

    PS : 串行隊列的第二個參數咱們傳NULL的效果是同樣的。多線程

  • 併發隊列(Concurrent)併發

    容許多個任務並行(同時)執行,可是執行任務的順序是隨機,具體要看CPU的調度狀況,這塊後續會說到。app

    dispatch_queue_t queue = dispatch_queue_create("Typeco", DISPATCH_QUEUE_CONCURRENT);
    複製代碼

    這裏同上,不過第二個參數咱們選擇 DISPATCH_QUEUE_CONCURRENT

  • 系統隊列

    • dispatch_get_main_queue() 主隊列是應用 程序啓動時(main函數以前),系統自動建立的惟一一個串行隊列,而且該隊列與主線程綁定。
    • dispatch_get_global_queue(0,0) 全局併發隊列,這裏一般咱們沒有特別需求的狀況下,默認都傳0便可。全局併發隊列只可獲取而不能建立。

知道了隊列的含義,咱們就瞭解了任務加到隊列中是如何執行的,接下來咱們就要看函數是如何影響隊列任務執行的。

函數

  • 同步函數 dispatch_sync()

    • 必須等待當前語句執行完畢,纔會執行下一條語句
    • 不會開啓線程,在當前線程執行block任務
    • 其執行順序依舊遵循當前隊列的FIFO原則
  • 異步函數 dispatch_async()

    • 不用等待當前語句執行完畢,就能夠執行下一條語句
    • 會開啓線程執行block的任務
    • 異步是多線程的代名詞
  • 線程和隊列的關係

    具體測試可前往 Demo 進行下載!

    區別 串行 併發 主隊列
    同步 1.不會開啓線程,在當前線程執行任務
    2.任務一個接一個執行
    3.會產生堵塞
    1.不會開啓線程,在當前線程執行任務
    2.任務一個接一個執行
    1.死鎖卡住不執行
    異步 1.開啓一條新線程
    2.任務一個接一個執行
    1.開啓線程,在當前線程執行任務
    2.任務異步執行,沒有順序,cpu調度有關
    1.不會開啓新線程,依舊在主線程串行執行任務

    因而可知,線程和隊列並無直接聯繫。

隊列和函數的底層實現

首先能夠去 GCD源碼 進行下載查看。

隊列的建立

咱們大概分析一下隊列是如何建立的,首先dispatch_queue_create的第一個參數是一個字符串爲了標識queue,第二個參數表明的是串行仍是併發,那麼咱們研究的重點就放在第二個參數上:

   /*
      初始方法,依次會調用下方核心代碼
     */

    dispatch_queue_create("sync_serial", DISPATCH_QUEUE_SERIAL);
    /*
        主要看下面的方法實現
        此處dqa便是上面的 DISPATCH_QUEUE_SERIAL
     */

    _dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
                                      dispatch_queue_t tq, bool legacy)



   // 此處省略大量代碼...



    // 開闢內存 - 生成響應的對象 queue
    dispatch_lane_t dq = _dispatch_object_alloc(vtable,
                                                sizeof(struct dispatch_lane_s));


    /*
      構造方法,此處有關鍵點,若是是dqai_concurrent 那麼隊列寬度 DISPATCH_QUEUE_WIDTH_MAX 不然爲 1
      也就是說串行隊列寬度爲1,併發沒有限制
     */

    _dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
                         DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
                         (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));

    // 標籤
    dq->dq_label = label;
    // 優先級
    dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
                                              dqai.dqai_relpri);
    /*
        此處對queue進行保留,api裏有描述若是非ARC 咱們建立完以後要調用 dispatch_release
     */

    _dispatch_retain(tq);

    return dq;
複製代碼

以上只是大概分析一下 dispatch_queue_create 是如何建立隊列以及隊列是如何區分串行和併發的,相關細節還請下載源碼進行閱讀。

信號量

研究信號量無非就是關注其三個方法:

dispatch_semaphore_create

 dispatch_semaphore_t
dispatch_semaphore_create(long value)
{
    dispatch_semaphore_t dsema;

    // If the internal value is negative, then the absolute of the value is
    // equal to the number of waiting threads. Therefore it is bogus to
    // initialize the semaphore with a negative value.
    if (value < 0) {
        return DISPATCH_BAD_INPUT;
    }

    dsema = _dispatch_object_alloc(DISPATCH_VTABLE(semaphore),
            sizeof(struct dispatch_semaphore_s));
    dsema->do_next = DISPATCH_OBJECT_LISTLESS;
    dsema->do_targetq = _dispatch_get_default_queue(false);
    dsema->dsema_value = value;
    _dispatch_sema4_init(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
    dsema->dsema_orig = value;
    return dsema;
}
複製代碼

其實就是一個初始化dispatch_semaphore_t並賦值的過程,其賦值主要在**dema_value**上,記住這個字段,可能咱們在後面的分析中要用到.

dispatch_semaphore_signal

 long
dispatch_semaphore_signal(dispatch_semaphore_t dsema)
{
    long value = os_atomic_inc2o(dsema, dsema_value, release);
    if (likely(value > 0)) {
        return 0;
    }
    if (unlikely(value == LONG_MIN)) {
        DISPATCH_CLIENT_CRASH(value,
                "Unbalanced call to dispatch_semaphore_signal()");
    }
    return _dispatch_semaphore_signal_slow(dsema);
}
複製代碼

os_atomic_inc2o ----> os_atomic_add2o(p, f, 1, m)----> os_atomic_add(&(p)->f, (v), m) ----> _os_atomic_c11_op((p), (v), m, add, +)----> 得出結果deem_value+1,也就是說上述os_atomic_inc2o返回的結果是在以前value的基礎上進行+1,若是>0當即返回0

dispatch_semaphore_wait

long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
{
    // value++
    long value = os_atomic_dec2o(dsema, dsema_value, acquire);
    if (likely(value >= 0)) {
        return 0;
    }
    return _dispatch_semaphore_wait_slow(dsema, timeout);
}
複製代碼

os_atomic_dec2o的原理跟上面那個os_atomic_inc2o差很少,只不過此次運算符是 - 號,因此總結一下就是,對信號量進行減1操做,若是大於等於0返回0

那麼縱觀這兩個方法給咱們看到的僅僅是對一個數字(信號量)進行加減的操做,那麼它在底層是如何影響咱們的線程的呢?

do {
        _dispatch_trace_runtime_event(worker_unpark, dq, 0);
        _dispatch_root_queue_drain(dq, pri, DISPATCH_INVOKE_REDIRECTING_DRAIN);
        _dispatch_reset_priority_and_voucher(pp, NULL);
        _dispatch_trace_runtime_event(worker_park, NULL0);
    } while (dispatch_semaphore_wait(&pqc->dpq_thread_mediator,
            dispatch_time(0, timeout)) == 0);
複製代碼

在queue.c文件中,找到了上述代碼,其實就是一個do..while 循環,只要wait返回0就一直讓queue處於一個推遲執行(park)的狀態,結合咱們上述wait方法的理解能夠得出,只要信號量爲>=0 當前隊列繼續FIFO,不然一直等待直到wait返回0,當執行signal方法以後信號量會執行+1操做,這個時候就會打破上述循環。

綜上咱們一般這麼使用信號量:

- (void)demo {
    dispatch_semaphore_t sema = dispatch_semaphore_create(1);
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(00), ^{
        // 耗時操做,異步操做
        sleep(5);
        dispatch_semaphore_signal(sema);
    });
}
複製代碼

初始化1的信號量,表明了單線程一次只能有一個線程訪問,好比當前咱們線程1已經執行了wait方法,這個時候信號量爲0,當另一個線程2來訪問這個異步耗時操做時候就會處於上述的do...while 循環等待中,直到線程1執行signal 對信號量執行+1操做,這個時候纔會打破這個循環讓線程2能訪問該操做。

這個是爲了解決防止多線程同時訪問同一個資源形成的安全問題,咱們還可使用信號量達到barrier (柵欄)的做用:

- (void)demo {
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(00), ^{
        // 執行方法1
        NSLog(@"執行方法1");
        sleep(5);
        dispatch_semaphore_signal(sema);
    });
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(00), ^{
       //執行方法2
        NSLog(@"執行方法2");
    });
}
複製代碼

此處惟一的區別的就是初始化的時候賦值爲0,默認wait等待,直到異步線程signal,才能越過wait執行後續的任務,達到一個柵欄的效果。

調度組

如下都是基於libdispatch源碼分析的結果: 源碼參考,源碼部份內容比較多,詳細代碼請下載查看,這裏儘可能少粘貼代碼,力求用流程圖來展現大概過程:

dispatch_group 相關代碼都在dispatch_semaphore.c模塊中,可見dispatch_group是一個基於信號量的同步機制,核心功能主要是下面幾個函數:

  • dispatch_group_enter
  • dispatch_group_leave
  • dispatch_group_wait
  • dispatch_group_async
  • dispatch_group_notify

圖中共有4組控制流:

  • 上方有兩條並行的async,異步執行而且內部會隱式調用enter和leave方法
  • 右上角是普通的enter和leave
  • 左下角是wait控制,使用了阻塞等待方式,一致等到信號符合
  • 右下角的notify有兩個分支,一致是判斷信號符合直接喚醒wait進行處理notify的block,若是條件不知足,則見notify的block放入group對應的queue中等到未來知足信號量的時候來觸發執行。

最好就是誰來喚醒notify,全部的async和普通的方式都會走enter和leave,因此信號的判斷就是enter和leave的配對,正如api所述,enter和leave要成對出現,若是使用async則不用擔憂,由於其內部幫你成對的實現了:

void
dispatch_group_async(dispatch_group_t group, dispatch_queue_t queuedispatch_block_t block)
{
    dispatch_retain(group);
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        block();
        dispatch_group_leave(group);
        dispatch_release(group);
    });
}
複製代碼

因此每條控制流執行leave的時候都要檢查信號量是否知足,若是知足則執行notify,不然等待,既然關鍵是leave方法的執行觸發notify,因此就能夠重點看看leave的實現:

void
dispatch_group_leave(dispatch_group_t dg)
{
    // The value is incremented on a 64bits wide atomic so that the carry for
    // the -1 -> 0 transition increments the generation atomically.
    uint64_t new_state, old_state = os_atomic_add_orig2o(dg, dg_state,
            DISPATCH_GROUP_VALUE_INTERVAL, release);
    uint32_t old_value = (uint32_t)(old_state & DISPATCH_GROUP_VALUE_MASK);

    if (unlikely(old_value == DISPATCH_GROUP_VALUE_1)) {
        old_state += DISPATCH_GROUP_VALUE_INTERVAL;
        do {
            new_state = old_state;
            if ((old_state & DISPATCH_GROUP_VALUE_MASK) == 0) {
                new_state &= ~DISPATCH_GROUP_HAS_WAITERS;
                new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
            } else {
                // If the group was entered again since the atomic_add above,
                // we can't clear the waiters bit anymore as we don't know for
                // which generation the waiters are for
                new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
            }
            if (old_state == new_state) break;
        } while (unlikely(!os_atomic_cmpxchgv2o(dg, dg_state,
                old_state, new_state, &old_state, relaxed)));
        return _dispatch_group_wake(dg, old_state, true);
    }

    if (unlikely(old_value == 0)) {
        DISPATCH_CLIENT_CRASH((uintptr_t)old_value,
                "Unbalanced call to dispatch_group_leave()");
    }
}
複製代碼

其中有一個do...while循環,每次對狀態進行比對,直到狀態符合調用wake方法喚醒group。

調度組使用

  • (異步請求1 + 異步請求2) ==> 異步請求3,請求3依賴請求1和請求2的返回

     dispatch_group_t group = dispatch_group_create();
        dispatch_group_async(group, dispatch_get_global_queue(00), ^{
            NSLog(@"異步請求1");
        });
        dispatch_group_async(group, dispatch_get_global_queue(00), ^{
            sleep(3);
            NSLog(@"異步請求2");
        });

        dispatch_group_notify(group, dispatch_get_global_queue(00), ^{
            NSLog(@"1和2執行完了異步請求3");
        });

        dispatch_group_async(group, dispatch_get_global_queue(00), ^{
            NSLog(@"異步請求4");
        });
    複製代碼

    GCD_Demo[19428:6087378] 異步請求1

    GCD_Demo[19428:6087375] 異步請求4

    GCD_Demo[19428:6087377] 異步請求2

    GCD_Demo[19428:6087377] 1和2執行完了異步請求3

    這裏我故意在notify後面多加了個4,發現notify這個是不分先後順序的。

  • enter + leave

    dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t queue = dispatch_get_global_queue(00);

        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"異步請求1");
            dispatch_group_leave(group);
        });

        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            sleep(3);
            NSLog(@"異步請求2");
            dispatch_group_leave(group);
        });


        dispatch_group_notify(group, dispatch_get_global_queue(00), ^{
            NSLog(@"1和2執行完了異步請求3");
        });
    複製代碼

    達到的效果是同樣的,只不過這裏沒有用到group_async 是同樣能達到相應目的。

至此GCD分析告一段落,想dispatch_source 以及柵欄函數相關後續還會陸續分享,歡迎留言交流。

我的小站 www.typeco.cn 歡迎光臨。

相關文章
相關標籤/搜索