19-探究iOS底層原理|多執行緒技術【GCD原始碼分析2:柵欄函式dispatch_barrier_(a)sync、訊號量dispatch_semaphore】

語言: CN / TW / HK

前言

之前,我們在探索動畫及渲染相關原理的時候,我們輸出了幾篇文章,解答了iOS動畫是如何渲染,特效是如何工作的疑惑。我們深感系統設計者在創作這些系統框架的時候,是如此腦洞大開,也 深深意識到了解一門技術的底層原理對於從事該方面工作的重要性。

因此我們決定 進一步探究iOS底層原理的任務。繼上一篇文章對GCDdispatch_get_global全域性併發佇列_queue+dispatch_sync同步函式dispatch_get_global全域性併發佇列_queue+dispatch_sync非同步函式GCD單例GCD執行緒死鎖探索之後,本篇文章將繼續對GCD多執行緒底層原理的探索

一、柵欄函式基本介紹dispatch_barrier_async與dispatch_barrier_sync

1.1 柵欄函式的作用

柵欄函式的作⽤

最直接的作⽤: 控制任務執⾏順序,也就是達到同步的效果

  • dispatch_barrier_async:前面的任務執行完畢,才會來到這裡
  • dispatch_barrier_sync:作用相同,但是這個會堵塞執行緒,影響後面的執行

注意 :柵欄函式只能控制同一併發佇列

1.2 柵欄函式使用舉例

  • dispatch_barrier_async 舉例

dispatch_barrier_async舉例

  • 執行結果如下:

控制檯列印結果

  • 在同一個佇列裡面,柵欄函式前面的任務執行完了,柵欄函式裡面的任務可以執行,但是不會堵塞執行緒
  • 柵欄函式後面的任務還是可以執行的。但是柵欄函式前面的任務,是一定在柵欄函式內部任務之前執行的。

也就是任務 1任務 2是必然在柵欄函式前面執行。

  • dispatch_barrier_sync

程式碼還是👆上面的程式碼,就是把柵欄函式非同步改成同步了,看看會發生什麼樣的效果?

dispatch_barrier_sync舉例

  • 控制檯列印結果如下:

列印結果

  • 柵欄函式前面的任務還是正常執行了,但是後面的任務在柵欄函式的後面執行
  • 柵欄函式堵塞了執行緒,柵欄函式後面的任務在柵欄函式的任務執行完成,才會去執行

還記得上面的一句話嗎:柵欄函式只能控制同一併發佇列,那麼我們試試不是同一個併發佇列情況,柵欄函式是否可以攔截住呢?

不是同一個佇列情況舉例

我們把柵欄函式放在了另一個併發的佇列裡面,發現並沒有攔截住任務的執行,那麼是不是非同步的原因呢?

那麼現在去改成同步看看能不能攔住呢?

不是同一個佇列情況舉例

從執行的結果來看,發現還是攔不住,說明不是同一個併發的佇列,不管柵欄函式是不是同步或者非同步,都是攔截不住的,只能是同一個併發佇列才可以!

我們再來舉個例子🌰,使用全域性併發佇列看看

全域性併發佇列舉例

從列印結果來看,全域性併發佇列也是攔不住的,只能是自定義併發佇列才可以,這是為什麼呢?去底層原始碼看看是否可以找到答案!

二、 柵欄函式原始碼分析

2.1 流程跟蹤

上面已經對柵欄函式的作用有一個大致的認識,那麼底層的實現邏輯是怎麼樣的呢?現在就去探索一下。

在原始碼裡面搜尋dispatch_barrier_sync,跟流程會走到_dispatch_barrier_sync_f-- > _dispatch_barrier_sync_f_inline

_dispatch_barrier_sync_f_inline

這個_dispatch_barrier_sync_f_inline 方法我們之前分析死鎖的時候來過這裡面,通過符號斷點,這裡會走_dispatch_sync_f_slow方法,這裡設定了DC_FLAG_BARRIER的標籤,對柵欄做標記!

_dispatch_sync_f_slow

這裡也是之前同步產生死鎖的時候來過的,通過下符號斷點繼續跟蹤流程。

符號斷點跟蹤除錯

由此跟蹤的流程為:_dispatch_sync_f_slow --> _dispatch_sync_invoke_and_complete_recurse --> _dispatch_sync_complete_recurse,繼續在原始碼裡面跟蹤發現定位到了這個_dispatch_sync_complete_recurse方法。

_dispatch_sync_complete_recurse

這裡是一個 do while迴圈,判斷當前佇列裡面是否有barrier,有的話就dx_wakeup喚醒執行,直到任務執行完成了,才會執行_dispatch_lane_non_barrier_complete,表示當前佇列任務已經執行完成了,並且沒有柵欄函數了就會繼續往下面的流程走。

```

define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)

```

那麼現在去看看dq_wakeup

dq_wakeup

這裡我們之前分析同步和非同步的時候也來過這裡,這裡全域性併發的是 _dispatch_root_queue_wakeup,序列和併發的是_dispatch_lane_wakeup,那麼兩者有什麼不一樣呢?

2.2 自定義的併發佇列分析

我們先去看看自定義的併發佇列的_dispatch_lane_wakeup

``` _dispatch_lane_wakeup(dispatch_lane_class_t dqu, dispatch_qos_t qos, dispatch_wakeup_flags_t flags) { dispatch_queue_wakeup_target_t target = DISPATCH_QUEUE_WAKEUP_NONE;

if (unlikely(flags & DISPATCH_WAKEUP_BARRIER_COMPLETE)) {
    return _dispatch_lane_barrier_complete(dqu, qos, flags);
}
if (_dispatch_queue_class_probe(dqu)) {
    target = DISPATCH_QUEUE_WAKEUP_TARGET;
}
return _dispatch_queue_wakeup(dqu, qos, flags, target);

} ```

  • 判斷是否為barrier形式的,會呼叫_dispatch_lane_barrier_complete方法處理
  • 如果沒有barrier形式的,則走正常的併發佇列流程,呼叫_dispatch_queue_wakeup方法。
  • _dispatch_lane_barrier_complete

_dispatch_lane_barrier_complete

  • 如果是序列佇列,則會進行等待,等待其他的任務執行完成,再按順序執行
  • 如果是併發佇列,則會呼叫_dispatch_lane_drain_non_barriers方法將柵欄之前的任務執行完成。
  • 最後會呼叫_dispatch_lane_class_barrier_complete方法,也就是把柵欄拔掉了,不攔了,從而執行柵欄之後的任務。

_dispatch_lane_class_barrier_complete

2.3 全域性併發佇列分析

  • 全域性併發佇列,dx_wakeup對應的是_dispatch_root_queue_wakeup方法,檢視原始碼實現

c++ void _dispatch_root_queue_wakeup(dispatch_queue_global_t dq, DISPATCH_UNUSED dispatch_qos_t qos, dispatch_wakeup_flags_t flags) { if (!(flags & DISPATCH_WAKEUP_BLOCK_WAIT)) { DISPATCH_INTERNAL_CRASH(dq->dq_priority, "Don't try to wake up or override a root queue"); } if (flags & DISPATCH_WAKEUP_CONSUME_2) { return _dispatch_release_2_tailcall(dq); } }

  • 全域性併發佇列這個裡面,並沒有對barrier的判斷和處理,就是按照正常的併發佇列來處理。
  • 全域性併發佇列為什麼沒有對柵欄函式進行處理呢?因為全域性併發佇列除了被我們使用,系統也在使用。
  • 如果添加了柵欄函式,會導致佇列執行的阻塞,從而影響系統級的執行,所以柵欄函式也就不適用於全域性併發佇列。

三、 訊號量dispatch_semaphore

3.1 訊號量介紹

訊號量在GCD中是指Dispatch Semaphore,是一種持有計數的訊號的東西。有如下三個方法。 - dispatch_semaphore_create : 建立訊號量 - dispatch_semaphore_wait : 訊號量等待 - dispatch_semaphore_signal : 訊號量釋放

3.2 訊號量舉例

在併發佇列裡面,可以使用訊號量控制,最大併發數,如下程式碼:

訊號量舉例

  • 訊號量舉例列印結果

訊號量舉例列印結果

這裡一共建立了 4 個任務,非同步併發執行,我在建立訊號量的時候,設定了最大併發數為2

c++ dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_semaphore_t sem = dispatch_semaphore_create(2);

從執行的動圖,可以看到,每次都是兩個任務一起執行了,列印的結果一目瞭然。

那麼再舉個例子看看,設定訊號量併發數為0會怎麼樣呢?

設定訊號量併發數為0 設定訊號量併發數為0,就相當於加鎖的作用,dispatch_semaphore_wait堵住了任務1讓其等待,等任務 2執行完了,dispatch_semaphore_signal傳送訊號,我執行完了,你去執行吧!

這樣到底訊號量是怎麼樣等待,又是怎麼樣傳送訊號的呢?

3.3 訊號量分析

看看dispatch_semaphore_createapi的說明

dispatch_semaphore_create

  • 當兩個執行緒需要協調特定事件的完成時,為該值傳遞0很有用。
  • 傳遞大於0的值對於管理有限的資源池很有用,其中池大小等於該值。
  • 訊號量的起始值。 傳遞小於訊號量的起始值。 傳遞小於零的值將導致返回 NULL。的值將導致返回 NULL,也就是小於0就不會正常執行。

總結來說,就是可以控制執行緒池中的最多併發數量

3.3.1 dispatch_semaphore_signal

  • dispatch_semaphore_signal

dispatch_semaphore_signal

  • dispatch_semaphore_signal裡面os_atomic_inc2o原子操作自增加1,然後會判斷,如果value > 0,就會返回0
  • 例如 value1之後還是小於0,說明是一個負數,也就是呼叫dispatch_semaphore_wait次數太多了,dispatch_semaphore_wait是做減操作的,等會後面會分析。
  • 加一次後依然小於0就報異常"Unbalanced call to dispatch_semaphore_signal(),然後會呼叫_dispatch_semaphore_signal_slow方法的,做容錯的處理,_dispatch_sema4_signal是一個do while 迴圈

c++ _dispatch_semaphore_signal_slow(dispatch_semaphore_t dsema) { _dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO); _dispatch_sema4_signal(&dsema->dsema_sema, 1); return 1; }

  • _dispatch_sema4_signal

c++ void _dispatch_sema4_signal(_dispatch_sema4_t *sema, long count) { do { int ret = sem_post(sema); DISPATCH_SEMAPHORE_VERIFY_RET(ret); } while (--count); }

3.3.2 dispatch_semaphore_wait

  • dispatch_semaphore_wait

dispatch_semaphore_wait原始碼如下: dispatch_semaphore_wait

  • os_atomic_dec2o進行原子自減1操作,也就是對value值進行減操作,控制可併發數。
  • 如果可併發數為2,則呼叫該方法後,變為1,表示現在併發數為 1,剩下還可同時執行1個任務。如果初始值是0,減操作之後為負數,則會呼叫_dispatch_semaphore_wait_slow方法。

_dispatch_semaphore_wait_slow方法原始碼如下:

  • _dispatch_semaphore_wait_slow

_dispatch_semaphore_wait_slow

  • 這裡對dispatch_time_t timeout 進行判斷處理,我們前面的例子裡面傳的是DISPATCH_TIME_FOREVER,那麼會呼叫_dispatch_sema4_wait方法

c++ void _dispatch_sema4_wait(_dispatch_sema4_t *sema) { kern_return_t kr; do { kr = semaphore_wait(*sema); } while (kr == KERN_ABORTED); DISPATCH_SEMAPHORE_VERIFY_KR(kr); }

_dispatch_sema4_wait方法裡面是一個do-while迴圈,當不滿足條件時,會一直迴圈下去,從而導致流程的阻塞。這也就解釋了上面舉例案裡面的執行結果。

上面舉例裡面就相當於,下圖中的情況

分析

在上圖框框的地方,① 相當於②,這裡是do-while迴圈,所以會執行任務 2任務 1一直在迴圈等待。

三、 總結

3.1 柵欄函式

  • 使用柵欄函式的時候,要和其他需要執行的任務必須在同一個佇列中
  • 使用柵欄函式不能使用全域性併發佇列
  • 除了我們使用,系統也在使用。
  • 如果添加了柵欄函式,會導致佇列執行的阻塞,影響系統級的執行

3.2 訊號量

  • dispatch_semaphore_wait訊號量等待,內部是對併發數做自減操作,如果為 小於0,會執行_dispatch_semaphore_wait_slow然後呼叫_dispatch_sema4_wait是一個do-while,知道滿足條件結束迴圈
  • dispatch_semaphore_signal 訊號量釋放 ,內部是對併發數做自加操作,直到大於0時,為可操作
  • 保持執行緒同步,將非同步執行任務轉換為同步執行任務
  • 保證執行緒安全,為執行緒加鎖,相當於互斥鎖
「其他文章」