單例
說起單例,我們一般使用GCD的dispath_once來創(chuàng)建單例對于單例侨拦,需要知道以下兩個問題:
- 1.單例為什么只執(zhí)行一次,底層是如何控制的
- 2.單例的block是在什么時候進行調(diào)用
下面我們來探究一下
單例為什么只執(zhí)行一次
再進入dispatch_once的源碼前旁理,我們先看下dispatch_once的參數(shù)
- 1.onceToken枫浙,這是一個
靜態(tài)變量
,由于不同位置定義的靜態(tài)變量是不同
的器紧,所以靜態(tài)變量具有唯一性
耀销。 - 2.block回到
我們看到會調(diào)用dispatch_once_f,其中val是外界傳入
的onceToken靜態(tài)變量
铲汪,而func是_dispatch_Block_invoke(block)熊尉,我們看下dispatch_once_f的底層實現(xiàn)
通過上面代碼,可以知道底層主要分為以下幾步
- 1.
將val掌腰,也就是靜態(tài)變量轉(zhuǎn)換為dispatch_once_gate_t類型變量l
- 2.通過
os_atomic_load獲取此時的任務的標識符v
- 3.如果
v等于DLOCK_ONCE_DON
E狰住,表示任務執(zhí)行過
了,只接return - 4.如果
任務執(zhí)行后齿梁,加鎖失敗
了催植,則走到_dispatch_once_mark_done_if_quiesced函數(shù),函數(shù)里再次進行存儲
勺择,將標識符置為DLOCK_ONCE_DONE
创南。 - 5.反之,則通過
_dispatch_once_gate_tryenter嘗試進入任務
省核,即解鎖稿辙,然后執(zhí)行_dispatch_once_callout執(zhí)行block回調(diào) - 6.如果此時
有任務正在執(zhí)行
,再有任務進來
气忠,則通過_dispatch_once_wait函數(shù)
讓新來的任務進入無限次等待
邻储。
單例block是什么時候調(diào)用
上面我們知道func就是任務block
,而處理func的方法就是_dispatch_once_callout
旧噪,前面判斷_dispatch_once_gate_tryenter解鎖
吨娜,我們看下_dispatch_once_gate_tryenter這個方法實現(xiàn)
其源碼主要是通過底層的os_atomic_cmpxchg方法進行對比
,如果比較沒有問題
淘钟,則進行加鎖宦赠,即任務的標識置為DLOCK_ONCE_UNLOCKED
。 我們下面看下_dispatch_once_callout方法源碼
上面方法主要分兩步:
- 1._dispatch_client_callout:block回調(diào)執(zhí)行
- 2._dispatch_once_gate_broadcast:進行廣播
再看下_dispatch_once_gate_broadcast方法實現(xiàn)_dispatch_client_callout主要執(zhí)行回調(diào),其中f就是傳入的_dispatch_Block_invoke(block)勾扭,即異步回調(diào)
進入 _dispatch_once_gate_broadcast -> _dispatch_once_mark_done源碼缤骨,主要就是
給dgo->dgo_once一個值
,然后將任務的標識符為DLOCK_ONCE_DONE
尺借,即解鎖
绊起。
單例總結(jié)
上面我們對單例進行了探索银锻,解開了上面所提出的問題躁绸。下面總結(jié)一下:
- 1.【單例執(zhí)行一次原理】:GCD單例中,有兩個重要參數(shù)熬词,
onceToken
和block
栅表,其中onceToken是靜態(tài)變量
笋鄙,具有唯一性
,在底層被封裝成了dispatch_once_gate_t類型的變量l
怪瓶,l主要是用來獲取底層原子封裝性的關聯(lián)萧落,即變量v,通過v來查詢?nèi)蝿盏臓顟B(tài)
洗贰,如果此時v等于DLOCK_ONCE_DONE
找岖,說明任務已經(jīng)處理過一次
了,直接return敛滋。 - 2.【block調(diào)用時機】:如果此時
任務沒有執(zhí)行過
许布,則會在底層通過C++函數(shù)的比較
,將任務進行加鎖
绎晃,即任務狀態(tài)置為DLOCK_ONCE_UNLOCK
蜜唾,目的是為了保證當前任務執(zhí)行的唯一性
,防止在其他地方有多次定義
庶艾。加鎖之后進行block回調(diào)函數(shù)的執(zhí)行袁余,執(zhí)行完成后,將當前任務解鎖
咱揍,將當前的任務狀態(tài)置為DLOCK_ONCE_DONE
颖榜,在下次進來時,就不會在執(zhí)行述召,會直接返回
- 3.【對多線程的印象】:如果在
當前任務執(zhí)行期間朱转,有其他任務進來蟹地,會進入無限次等待
积暖,原因是當前任務已經(jīng)獲取了鎖
,進行了加鎖怪与,其他任務是無法獲取鎖
的夺刑。
柵欄函數(shù)
GCD中我們有時候會用到柵欄函數(shù)來確定任務順序,柵欄任務主要有兩種
- 1.
同步柵欄函數(shù)dispatch_barrier_sync
(在主線程中執(zhí)行):前面的任務執(zhí)行完畢才會來到這里,但是同步柵欄函數(shù)會堵塞線程
桅咆,影響后面的任務執(zhí)行 - 2.
異步柵欄函數(shù)dispatch_barrier_async
:前面的任務執(zhí)行完畢才會來到這里
柵欄函數(shù)最直接的作用就是控制任務執(zhí)行順序
薛夜,保證任務按計劃順序執(zhí)行
。
柵欄函數(shù)有幾下幾點需要注意:
- 1.柵欄函數(shù)
只能
控制同一
并發(fā)隊列 - 2.
同步柵欄添加進入隊列
的時候梯澜,當前線程會被鎖死
奴迅,直到同步柵欄之前的任務
和同步柵欄任務本身執(zhí)行完畢
時,當前線程才會打開然后繼續(xù)執(zhí)行下一句代碼
挺据。 - 3.在
使用柵欄函數(shù)時.使用自定義隊列才有意義
,如果用的是串行隊列或者系統(tǒng)提供的全局并發(fā)隊列
,這個柵欄函數(shù)的作用等同于一個同步函數(shù)的作用,沒有任何意義
暇检。
異步柵欄函數(shù)
通過打印我們知道
異步柵欄函數(shù)不會阻塞主線程
婉称,堵塞的是異步函數(shù)對列
构蹬。
同步柵欄函數(shù)
同步柵欄函數(shù)會堵塞主線程
悔据,也會堵塞當前線程
庄敛。
柵欄函數(shù)總結(jié)
- 1.
異步柵欄函數(shù)阻塞的是隊列
,而且必須是自定義的并發(fā)隊列
隐绵,不影響主線程任務的執(zhí)行
依许。 - 2.
同步柵欄函數(shù)阻塞的是線程峭跳,且是主線程
蛀醉,會影響主線程其他任務的執(zhí)行
。
使用場景
柵欄函數(shù)除了用于控制任務的執(zhí)行順序
垛玻,還可以用于數(shù)據(jù)安全
。
下面我們添加柵欄函數(shù)崩潰原因:
數(shù)據(jù)不斷的retain和release
,在數(shù)據(jù)還沒有retain完畢時郭蕉,已經(jīng)開始了realse
,相當于對一個空數(shù)據(jù)烟勋,進行realse
卵惦。
奔潰原因和上面一樣,原因是
柵欄函數(shù)對系統(tǒng)的全局隊列也會阻塞
,而系統(tǒng)其他地方也會用到全局隊列啡捶,此時就會崩潰
瞎暑。
除了使用柵欄函數(shù),還可以使用互斥鎖@synchronized (self) {}這樣就沒有任何問題
之所以
使用self,是因為self的生命周期大于i和mArray
逢并,這樣就保證synchronized不會關聯(lián)一個被銷毀的對象
。但是慎用@synchronized(self)
辩恼,這種方式很粗糙
,容易導致死鎖
聘萨。
柵欄函數(shù)注意問題
- 1.如果
柵欄函數(shù)中使用全局隊列
胸完,運行會崩潰
赊窥,原因是系統(tǒng)也在用全局并發(fā)隊列
,使用柵欄同時會攔截系統(tǒng)的址遇,所以會崩潰
- 2.如果將
自定義并發(fā)隊列改為串行隊列
,即serial 跺株,串行隊列本身就是有序同步
此時加柵欄
,會浪費性能
袖扛。 - 3.
柵欄函數(shù)只會阻塞一次
蛆封。
異步柵欄函數(shù) 底層分析
進入dispatch_barrier_async源碼實現(xiàn)
,其底層的實現(xiàn)與dispatch_async類似
砸讳,這里就不再做分析了,有興趣的可以自行探索下
同步柵欄函數(shù)底層分析
進入dispatch_barrier_sync
源碼,實現(xiàn)如下
下面我們看下_dispatch_barrier_sync_f_inline方法實現(xiàn)dispatch_barrier_sync調(diào)用_dispatch_barrier_sync_f平绩,而后調(diào)用_dispatch_barrier_sync_f_inline源碼。
方法實現(xiàn)分以下幾步:
- 1.通過
_dispatch_tid_self獲取線程ID
腹忽。 - 2.通過
_dispatch_queue_try_acquire_barrier_sync
判斷線程狀態(tài)嘹锁。
下面看下_dispatch_queue_try_acquire_barrier_sync
實現(xiàn)
通過源碼我們發(fā)現(xiàn)進入_dispatch_queue_try_acquire_barrier_sync_and_suspend
米同,然后在這里進行釋放 回到_dispatch_barrier_sync_f_inline方法,看1791行:_dispatch_sync_recurse方法
通過上面我們知道:
- 1.通過
_dispatch_sync_recurse
,遞歸查找柵欄函數(shù)的target
。 - 2.通過
_dispatch_introspection_sync_begin
對向前信息進行處理
柄驻。
信號量
信號量的作用一般是用來使任務同步執(zhí)行
答憔,類似于互斥鎖
,用戶可以根據(jù)需要控制GCD最大并發(fā)數(shù)
蓉驹,一般是這樣使用的
dispatch_semaphore_create 創(chuàng)建
該函數(shù)的底層實現(xiàn)如下狠持,主要是用來初始化信號量
喘垂,并設置GCD的最大并發(fā)數(shù)
,其最大并發(fā)數(shù)必須大于0
章贞。
dispatch_semaphore_wait 加鎖
該函數(shù)的源碼實現(xiàn)看到两踏,其主要作用是對信號量dsema
通過os_atomic_dec2o
進行了--
操作梦染,其內(nèi)部是執(zhí)行的C++的atomic_fetch_sub_explicit
方法隧甚。
- 1.如果
value 大于等于0
,表示操作無效帽借,即執(zhí)行成功
。 - 2.如果
value 等于LONG_MIN
脆荷,系統(tǒng)會拋出一個crash
梦皮。 - 3.如果
value 小于0,則進入長等待
让网。
將具體的值帶入為
os_atomic_dec2o(dsema, dsema_value, acquire);
os_atomic_sub2o(dsema, dsema_value, 1, m)
os_atomic_sub(dsema->dsema_value, 1, m)
_os_atomic_c11_op(dsema->dsema_value, 1, m, sub, -)
_r = atomic_fetch_sub_explicit(dsema->dsema_value, 1),
等價于 dsema->dsema_value - 1
_dispatch_semaphore_wait_slow
進入
_dispatch_semaphore_wait_slow的源碼實現(xiàn)
丸凭,當value小于0時
铛碑,根據(jù)等待事件timeout做出不同操作
汽烦。
dispatch_semaphore_signal 解鎖
該函數(shù)的源碼實現(xiàn)可以知道俗冻,其核心也是
通過os_atomic_inc2o函數(shù)
對value進行了++操作
,os_atomic_inc2o內(nèi)部是通過C++的atomic_fetch_add_explicit
。
- 1.如果value 大于 0冶伞,表示操作無效,即
執(zhí)行成功
芋类。 - 2.如果value 等于0榛瓮,則進入
長等待
。
其中os_atomic_dec2o
的宏定義轉(zhuǎn)換如下
將具體的值帶入:
os_atomic_inc2o(dsema, dsema_value, release);
os_atomic_add2o(dsema, dsema_value, 1, m)
os_atomic_add(&(dsema)->dsema_value, (1), m)
_os_atomic_c11_op((dsema->dsema_value), (1), m, add, +)
_r = atomic_fetch_add_explicit(dsema->dsema_value, 1),
等價于 dsema->dsema_value + 1
信號量總結(jié)
- 1.
dispatch_semaphore_create
主要就是初始化限號量
顷级。 - 2.
dispatch_semaphore_wait
是對信號量的value進行--
帽芽,即加鎖操作
导街。 - 3.
dispatch_semaphore_signal
是對信號量的value進行++
,即解鎖操作
泽论。
調(diào)度組(線程組)
線程組使用
調(diào)度組的最直接作用是控制任務執(zhí)行順序说订,常見的方式如下
dispatch_group_create 創(chuàng)建組
dispatch_group_async 進組任務
dispatch_group_notify 進組任務執(zhí)行完畢通知 dispatch_group_wait 進組任務執(zhí)行等待時間
//進組和出組需要是成對使用的钙姊,不然會有問題
dispatch_group_enter 進組
dispatch_group_leave 出組
我們看下如何使用 將dispatch_group_notify移到最前面
通過上面三圖我們可以知道膊毁,
dispatch_group_notify前移會導致調(diào)度組失效
描焰,第三圖和第四圖可以知道荆秦,dispatch_group_enter可以單獨存在
,而dispatch_group_leave必須和dispatch_group_enter成對出現(xiàn),否則報錯刑桑,報錯有延遲是因為async是并發(fā)病梢,會有延遲
蜓陌。
多加一個dispatch_group_enter
此時
不會執(zhí)行notify
,原因是少了一個leave
飒责,會讓notify一直等待
。
底層源碼
dispatch_group_create 創(chuàng)建組
作用:創(chuàng)建group
,并設置屬性嗅义,此時的group的value為0
蝙眶。
查看dispatch_group_create
源碼
上面方法執(zhí)行:
dispatch_group_create->_dispatch_group_create_with_count
武通,其中_dispatch_group_create_with_count是對group對象進行賦值
尾菇,并返回group對象派诬,其中n值為0默赂。
dispatch_group_enter 進組
看下dispatch_group_enter通過os_atomic_sub_orig2o對dg->dg.bits 作 --操作疾捍,對數(shù)值進行處理
dispatch_group_leave 出組
看下dispatch_group_leave源碼源碼進行如下操作
- 1.-1到0乱豆,即++操作
- 2.根據(jù)狀態(tài)奖恰,do-while循環(huán),喚醒執(zhí)行block任務
- 3.如果0 + 1 = 1宛裕,enter-leave不平衡瑟啃,即leave多次調(diào)用,會崩潰
執(zhí)行過程:
- 1.do-while循環(huán)進行
異步
命中 - 2.
_dispatch_continuation_async執(zhí)行任務
- 3.
_dispatch_wake_by_address開始地址釋放
- 4.
_dispatch_release_n引用釋放
這步與
異步函數(shù)的block回調(diào)執(zhí)行時一致
的蛹屿,不過多解釋
dispatch_group_notify 通知
查看dispatch_group_notify源碼實現(xiàn)通過上面我們知道:
如果old_state等于0
,就可以進行釋放
了疲酌,除了leave可以通過_dispatch_group_wake喚醒
蜡峰,其中dispatch_group_notify也可以喚醒
的了袁。
其中os_mpsc_push_update_tail是宏定義,用于獲取dg的狀態(tài)碼
湿颅。
dispatch_group_async
查看dispatch_group_async源碼可以看到dispatch_group_async方法主要做了兩件事:
- 1.
包裝任務
- 2.
異步處理任務
方法主要封裝了
dispatch_group_enter進組操作
载绿,之后調(diào)用_dispatch_continuation_async
方法,這個方法在執(zhí)行l(wèi)eave中的_dispatch_group_wake方法里也調(diào)用
了油航。都是進行常規(guī)的異步函數(shù)底層操作
崭庸。
猜想:上面我們知道enter和leave是成對出現(xiàn)
,所以block執(zhí)行
之后可能隱性的執(zhí)行l(wèi)eave
谊囚,通過斷點調(diào)試怕享,打印堆棧信息
通過堆棧信息,我們看到執(zhí)行_dispatch_client_callout
后執(zhí)行銷毀方法_dispatch_call_block_and_release
镰踏。我們看下_dispatch_client_callout源碼
完美印證dispatch_group_async底層調(diào)用了enter-leave
調(diào)度組總結(jié)
- 1.
enter-leave
只要成對出現(xiàn)就可以
函筋,不分前后,距離
(同一作用域) - 2.
dispatch_group_enter
在底層是通過C++函數(shù)
奠伪,對group的value執(zhí)行--
操作(即0 -> -1) - 3.
dispatch_group_leave
在底層是通過C++函數(shù)
跌帐,對group的value進行++
操作(即-1 -> 0) - 4.
dispatch_group_notif
y在底層主要是判斷group的state是否等于0
,當等于0
時绊率,就通知喚醒
- 5.
block任務的喚醒
谨敛,可以通過dispatch_group_leave
,也可以通過dispatch_group_notify
- 6.
dispatch_group_async
其底層的調(diào)用了enter和leave
dispatch_source
dispatch_source 定義
定義:dispatch_source
是基礎數(shù)據(jù)類型
滤否,用于協(xié)調(diào)特定底層系統(tǒng)事件的處理
脸狸,其CPU負荷較小
,占用很少資源
藐俺,具有聯(lián)結(jié)優(yōu)勢
炊甲。
dispatch_source
替代了異步回調(diào)函數(shù)
,來處理系統(tǒng)相關的事件
紊搪,當配置一個dispatc
h時蜜葱,你需要指定監(jiān)測的事件
、dispatch queue
耀石、以及處理事件的代碼(block或函數(shù))
。當事件發(fā)生時爸黄,dispatch source
會提交你的block或函數(shù)
到指定的queue去執(zhí)行
滞伟。
使用 Dispatch Source
而不使用 dispatch_async
的唯一原因就是利用聯(lián)結(jié)的優(yōu)勢
。
dispatch_source流程
在任一線程上調(diào)用
它的一個函數(shù)dispatch_source_merge_data后
炕贵,會執(zhí)行Dispatch Source
事先定義好的句柄
(可以把句柄簡單理解為一個block
)梆奈,這個過程叫Custom event
,用戶事件是dispatch source
支持處理的一種事件称开。
簡單來說就是:事件是由你調(diào)用 dispatch_source_merge_data 函數(shù)來向自己發(fā)出的信號亩钟。
句柄
:指向指針的指針
乓梨,它指向的局勢一個類或者結(jié)構,它和系統(tǒng)有密切的關系清酥,這當中還有通用句柄扶镀,就是HANDLE。它有一下幾類
- 1.實例句柄 HINSTANCE
- 2.位圖句柄 HBITMAP
- 3.設備表句柄 HDC
- 4.圖標句柄 HICON
使用
創(chuàng)建dispatch
- 1.
type
:dispatch源可處理的事件 - 2.
handle
:理解為句柄焰轻、索引或id臭觉,假如要監(jiān)聽進程,需要傳入進程的ID - 3.
mask
:理解為描述辱志,提供更詳細的描述蝠筑,讓它知道具體要監(jiān)聽什么 - 4.
queue
:自定義源需要的一個隊列,用來處理所有的響應句柄
Dispatch Source 種類
其中type的類型有一下幾種:
type(種類) | 說明 |
---|---|
DISPATCH_SOURCE_TYPE_DATA_ADD | 自定義的事件揩懒,變量增加 |
DISPATCH_SOURCE_TYPE_DATA_OR | 自定義的事件什乙,變量OR |
DISPATCH_SOURCE_TYPE_MACH_SEND | MACH端口發(fā)送 |
DISPATCH_SOURCE_TYPE_MACH_RECV | MACH端口接收 |
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE | 內(nèi)存壓力 (注:iOS8后可用) |
DISPATCH_SOURCE_TYPE_PROC | 進程監(jiān)聽,如進程的退出、創(chuàng)建一個或更多的子線程已球、進程收到UNIX信號 |
DISPATCH_SOURCE_TYPE_READ | IO操作稳强,如對文件的操作、socket操作的讀響應 |
DISPATCH_SOURCE_TYPE_SIGNAL | 接收到UNIX信號時響應 |
DISPATCH_SOURCE_TYPE_TIMER | 定時器 |
DISPATCH_SOURCE_TYPE_VNODE | 文件狀態(tài)監(jiān)聽和悦,文件被刪除退疫、移動、重命名 |
DISPATCH_SOURCE_TYPE_WRITE | IO操作鸽素,如對文件的操作褒繁、socket操作的寫響應 |
上面說了不少類型,我們需要注意兩個類型:
- 1.
DISPATCH_SOURCE_TYPE_DATA_ADD
:當同一時間
馍忽,一個事件
的的觸發(fā)頻率很高
棒坏,那么Dispatch Source
會將這些響應
以ADD的方式
進行累積
,然后等系統(tǒng)空閑
時最終處理
遭笋,如果觸發(fā)頻率
比較零散
坝冕,那么Dispatch Source
會將這些事件分別響應
。 - 2.
DISPATCH_SOURCE_TYPE_DATA_OR
:是自定義
的事件瓦呼,但是它是以OR的方式
進行累積
喂窟。
常用函數(shù)
//掛起隊列
dispatch_suspend(queue)
//分派源創(chuàng)建時默認處于暫停狀態(tài),在分派源分派處理程序之前必須先恢復
dispatch_resume(source)
//向分派源發(fā)送事件央串,需要注意的是磨澡,不可以傳遞0值(事件不會被觸發(fā)),同樣也不可以傳遞負數(shù)质和。
dispatch_source_merge_data
//設置響應分派源事件的block稳摄,在分派源指定的隊列上運行
dispatch_source_set_event_handler
//得到分派源的數(shù)據(jù)
dispatch_source_get_data
//得到dispatch源創(chuàng)建,即調(diào)用dispatch_source_create的第二個參數(shù)
uintptr_t dispatch_source_get_handle(dispatch_source_t source);
//得到dispatch源創(chuàng)建饲宿,即調(diào)用dispatch_source_create的第三個參數(shù)
unsigned long dispatch_source_get_mask(dispatch_source_t source);
//取消dispatch源的事件處理--即不再調(diào)用block厦酬。如果調(diào)用dispatch_suspend只是暫停dispatch源胆描。
void dispatch_source_cancel(dispatch_source_t source);
//檢測是否dispatch源被取消,如果返回非0值則表明dispatch源已經(jīng)被取消
long dispatch_source_testcancel(dispatch_source_t source);
//dispatch源取消時調(diào)用的block仗阅,一般用于關閉文件或socket等昌讲,釋放相關資源
void dispatch_source_set_cancel_handler(dispatch_source_t source, dispatch_block_t cancel_handler);
//可用于設置dispatch源啟動時調(diào)用block,調(diào)用完成后即釋放這個block霹菊。也可在dispatch源運行當中隨時調(diào)用這個函數(shù)剧蚣。
void dispatch_source_set_registration_handler(dispatch_source_t source, dispatch_block_t registration_handler);
使用場景
經(jīng)常用于驗證碼倒計
時,因為dispatch_source不依賴于Runloop
旋廷,而是直接和底層內(nèi)核交互
鸠按,準確性更高
。
寫到最后
文章我們分析了單例饶碘,柵欄函數(shù)目尖,信號量,調(diào)度組扎运,以及dispatch_source
瑟曲,主要對單例,柵欄函數(shù)豪治,信號量洞拨,調(diào)度組的實現(xiàn)以及查看了其實現(xiàn)的底層原理。線程的源碼比較難理解负拟,有興趣的可以去官方下載源碼烦衣,自己操作理解一下。內(nèi)容比較多掩浙,有些地方?jīng)]有詳細的去說明花吟,有不嚴謹?shù)牡胤较M魑恢赋?最近分析鎖的底層實現(xiàn),有時間會寫出來