Event Loop
Event Loop
事件循環(huán)機(jī)制,如javascript
的事件循環(huán)裹驰,以及依賴其的nodejs
都是采用的異步事件循環(huán)機(jī)制隧熙。
對于上述兩者,都是基于多線程幻林,但是都是單線程執(zhí)行任務(wù)代碼贞盯,其依賴的就是Event Loop
事件循環(huán)機(jī)制,通過事件隊列注冊事件及事件的觀察者沪饺,事件的執(zhí)行交由其他線程去執(zhí)行(如I/O操作邻悬,網(wǎng)絡(luò)請求等),nodejs
采用的是libuv
異步I/O線程池庫随闽;對于非異步I/O操作,如setTimeOut
setInterval
等肝谭,都是基于事件循環(huán)查詢(每次事件處理完成后進(jìn)入下一次事件循環(huán)時都會查看時間是否已到達(dá)掘宪,并且是任務(wù)是插入到任務(wù)隊列尾部蛾扇,因此存在誤差,不過也可采用process.netxTick
會將事件插入到事件循環(huán)前解析執(zhí)行魏滚,且可嵌套執(zhí)行)镀首;
- 定時器:本階段執(zhí)行已經(jīng)被
setTimeout()
和setInterval()
的調(diào)度回調(diào)函數(shù)。- 待定回調(diào):執(zhí)行延遲到下一個循環(huán)迭代的 I/O 回調(diào)鼠次。
- idle, prepare:僅系統(tǒng)內(nèi)部使用更哄。
- 輪詢:檢索新的 I/O 事件;執(zhí)行與 I/O 相關(guān)的回調(diào)(幾乎所有情況下,除了關(guān)閉的回調(diào)函數(shù)腥寇,那些由計時器和
setImmediate()
調(diào)度的之外)成翩,其余情況 node 將在適當(dāng)?shù)臅r候在此阻塞。- 檢測:
setImmediate()
回調(diào)函數(shù)在這里執(zhí)行赦役。- 關(guān)閉的回調(diào)函數(shù):一些關(guān)閉的回調(diào)函數(shù)麻敌,如:
socket.on('close', ...)
。
JavaScript單線程異步的背后——事件循環(huán)機(jī)制
JavaScript 運行機(jī)制詳解:再談Event Loop
Mach Port
這個在《unix進(jìn)程間通信》中已闡述掂摔,在這不過多闡述术羔;
RunLoop
RunLoop
就像其名字一樣,就是運行循環(huán)乙漓,核心就是事件循環(huán)+mach port级历,利用事件循環(huán)注冊觀察相應(yīng)的事件,若無事件處理叭披,線程就去睡眠等待內(nèi)核事件觸發(fā)或者通過手動喚醒寥殖,不停地循環(huán)處理各種事件(如timer
source0
source1
事件以及dispatch
分發(fā)的func block
等);
注:以下代碼分析基于CF-1151.16源碼趋观;
runloop與線程的關(guān)系
直接上結(jié)論:runloop
與線程是一一對應(yīng)的扛禽,且對于主線程是默認(rèn)開啟的,對于其他線程皱坛,需要通過手動開啟编曼,且只能通過蘋果對外的接口獲取線程相應(yīng)的CFRunLoopRef
對象:
CFRunLoopRef CFRunLoopGetMain(void);
CFRunLoopRef CFRunLoopGetCurrent(void);
原理就是:蘋果維護(hù)了一個全局的字典對象,若字典中不存在線程對應(yīng)的runloop
對象就會創(chuàng)建并賦值剩辟,并且還利用線程私有數(shù)據(jù)(數(shù)組)存儲了指定__CFTSDKeyRunLoop
當(dāng)前線程的CFRunLoopRef
對象(同時也關(guān)聯(lián)了runloop
對象銷毀的回調(diào)掐场,用于線程退出銷毀);策略是:優(yōu)先從線程私有數(shù)據(jù)數(shù)組中獲取贩猎,若獲取不到就從全局字典對象中獲取熊户,若無則去創(chuàng)建;
runloop對象結(jié)構(gòu)分析
CoreFoundation
中關(guān)于runloop
的五個類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中CFRunLoopRef
對象結(jié)構(gòu)體如下:
//CFRunLoop結(jié)構(gòu)體結(jié)構(gòu)
struct __CFRunLoop
{
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;//綁定的線程pthread_t
uint32_t _winthread;
CFMutableSetRef _commonModes;//通用的mode set集合
CFMutableSetRef _commonModeItems;//通用mode的itme集合
CFRunLoopModeRef _currentMode;//當(dāng)前mode
CFMutableSetRef _modes;//所有的mode
struct _block_item *_blocks_head;//添加的block任務(wù)吭服,與dispatch分發(fā)的block處理不同
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
runloop
對象的結(jié)構(gòu)體為__CFRunLoop
嚷堡,其存儲著runloop
相關(guān)的鎖保證線程安全(注意下NSRunLoop不是線程安全的),喚醒端口(用于CFRunLoopWakeUp
外部接口調(diào)用艇棕,主要是source0
)蝌戒,綁定的線程串塑,mode
各種集合(下面會重點闡述),block
處理任務(wù)(通過CFRunLoopPerformBlock
接口注冊的)以及記錄需要的相關(guān)信息(如運行時間_runTime
_sleepTime
)等北苟;
CFRunLoopModeRef
CFRunLoopRef
對象中包含了若干Mode
桩匪,Mode
對象的數(shù)據(jù)結(jié)構(gòu)如下:
struct __CFRunLoopMode
{
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;//mode名稱
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;//source0對象
CFMutableSetRef _sources1;//source1對象
CFMutableArrayRef _observers;//observer對象
CFMutableArrayRef _timers;//定時器
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;//需要監(jiān)聽的所有mach port集合
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
//mk_timer由mach port實現(xiàn)<https://opensource.apple.com/source/xnu/xnu-3789.51.2/osfmk/kern/mk_timer.c>
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
CFRunLoopModeRef
對象未對外暴露,可通過CFRunLoopCopyAllModes
及CFRunLoopCopyCurrentMode
獲取所有runloop
相關(guān)的Mode
友鼻,通過CFRunLoopAddCommonMode
添加Mode
傻昙;
runloop
可添加多個Mode
,但只能指定一個Mode
模式運行(默認(rèn)是kCFRunLoopDefaultMode
)彩扔,且需要退出當(dāng)前重新指定運行才能生效妆档;每個Mode
中可添加若干source
、timer
借杰、observer
过吻;
對于CoreFoundation
中的CFRunLoop
蘋果只提供了兩種默認(rèn)模式kCFRunLoopDefaultMode
和kCFRunLoopCommonModes
,其中kCFRunLoopCommonModes
只是操作common
標(biāo)記的字符串蔗衡,用于向所有現(xiàn)有的Modes
中添加相應(yīng)的觀察者纤虽,不是一種具體的Mode
,不能直接用于CFRunLoopRunInMode
調(diào)用運行绞惦;但上層NSRunLoopMode封裝了一些相應(yīng)的Mode
逼纸,如
-
NSDefaultRunLoopMode
即kCFRunLoopDefaultMode
默認(rèn)的模式 -
NSEventTrackingRunLoopMode
模態(tài)跟蹤事件時,例如鼠標(biāo)拖動循環(huán)济蝉,應(yīng)將運行循環(huán)設(shè)置為此模式杰刽; -
NSModalPanelRunLoopMode
運行等待模態(tài)窗口(如NSSavePanel
NSOpenPanel
)的輸入時指定; -
UITrackingRunLoopMode
運行控件追蹤時指定王滤,如UIScrollView
滑動時(這個系統(tǒng)會默認(rèn)自動切換到此模式)
iOS
應(yīng)用啟動時系統(tǒng)默認(rèn)注冊了5個Mode:
-
kCFRunLoopDefaultMode
: App的默認(rèn) Mode贺嫂,通常主線程是在這個Mode
下運行的; -
UITrackingRunLoopMode
: 界面跟蹤 Mode雁乡,用于 ScrollView 追蹤觸摸滑動第喳,保證界面滑動時不受其他 Mode 影響; -
UIInitializationRunLoopMode
: 在剛啟動 App 時第進(jìn)入的第一個 Mode踱稍,啟動完成后就不再使用曲饱; -
GSEventReceiveRunLoopMode
: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到珠月; -
kCFRunLoopCommonModes
: 這是一個占位的 Mode扩淀,沒有實際作用;
CFRunLoopSourceRef
CFRunLoopSourceRef
對象的數(shù)據(jù)結(jié)構(gòu)如下:
struct __CFRunLoopSource
{
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
mach_port_t (*getPort)(void *info);
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
} CFRunLoopSourceContext1;
其中聯(lián)合體union
中的versionx.version
字段信息用于區(qū)分source0
或者source1
啤挎;
-
source0
驻谆,結(jié)構(gòu)體中context
上下文中包含了各種回調(diào)函數(shù),主要是perform
回調(diào)函數(shù)(用于執(zhí)行添加到source0
中的任務(wù)),當(dāng)調(diào)用CFRunLoopSourceSignal
時會標(biāo)記__CFRunLoopSource
中的_bits
標(biāo)記位旺韭,然后調(diào)用CFRunLoopWakeUp
來喚醒runloop
再下一個循環(huán)中處理此回調(diào)氛谜;主要用于APP內(nèi)部事件,由APP負(fù)責(zé)管理觸發(fā)区端,如
UIEvent
事件; source1
澳腹,不同于source0
執(zhí)行回調(diào)函數(shù)织盼,source1
還需要指定mach port
,用于監(jiān)聽系統(tǒng)內(nèi)核事件或其他線程發(fā)來的事件酱塔;
CFRunLoopTimer
struct __CFRunLoopTimer
{
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
CFRunLoopTimer
結(jié)構(gòu)中包含了時間相關(guān)的變量沥邻,runloop
被timer
事件觸發(fā)都會去檢查當(dāng)前所有的timer
時間點是否達(dá)到哀军,若達(dá)到則處理事件任務(wù)译柏;具體觸發(fā)時間事件主要包含兩種mk_timer
及dispatch source
形式,兩者都是基于mach port
但是觸發(fā)runloop
并處理回調(diào)的處理方式不同接癌;
mk_timer
是通過__CFRunLoopDoTimers
來處理蕊玷,依賴于runloop
來觸發(fā)時間回調(diào)函數(shù)邮利,因此基于此的NStimer
及performSelector:withObject:afterDelay:
(是對NSTimer
的包裝),都需要runloop
運行垃帅;
而dispatch source
(針對主隊列延届,其他隊列不是通過runloop來觸發(fā))是通過__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
中指定的外部函數(shù)_dispatch_main_queue_callback_4CF
來處理,調(diào)用堆棧如下
dispatch_after
指定主隊列的時間任務(wù)贸诚,是對dispatch_source
的包裝方庭,對于存在UI的應(yīng)用默認(rèn)主隊列的runloop
是開啟的,若是其他工具類應(yīng)用酱固,則需要手動開啟主隊列runloop
械念,否則指定主隊列的dispatch source
是無法生效的;
Timer有兩種實現(xiàn)方式分別是MK_Timer和GCD Timer,在runloop中Timer被轉(zhuǎn)為了一個存了觸發(fā)時間的列表运悲,這個觸發(fā)時間是一個絕對時間龄减,會按時間大小升序排序,在最小的時間被觸發(fā)后扇苞,Runloop會更新列表保證時間始終是升序排列欺殿。如果Runloop在某次運行中阻塞了很長時間,Timer的觸發(fā)會受到影響鳖敷。過期的時間點會被移除而不會去觸發(fā)脖苏。
具體的NSTimer
與GCD Timer
實現(xiàn)剖析可參考從NSTimer的失效性談起(二):關(guān)于GCD Timer和libdispatch
不過在源碼中的USE_DISPATCH_SOURCE_FOR_TIMERS
未生效,暫時未搞清問題定踱,待后續(xù)補(bǔ)充棍潘;
CFRunLoopObserver
struct __CFRunLoopObserver
{
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
CFRunLoopObserver
觀察者對象指定了runloop
相應(yīng)狀態(tài)變化(_activities
指定需要觀察的類型)及狀態(tài)變化的回調(diào)指針_callout
,具體觀察的選項包括:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
RunLoop事件循環(huán)
runloop
事件循環(huán)的邏輯如上圖,對于黃色的Block
是通過CFRunLoopPerformBlock
添加的block
任務(wù)亦歉,若runloop
任務(wù)處理完成后就會休眠等待source1
恤浪、timer
、或者手動被喚醒來繼續(xù)下一次循環(huán)處理任務(wù)肴楷;
RunLoop實踐
AutoReleasePool
主要是利用runloop observer
觀察注冊的事件:kCFRunLoopEntry
水由、kCFRunLoopBeforeWaiting
、kCFRunLoopExit
赛蔫,分別用于autoreleasepool
push/pop
操作來創(chuàng)建/釋放內(nèi)存池砂客,并且保證自動內(nèi)存池創(chuàng)建優(yōu)先級其他回調(diào)之前,釋放內(nèi)存池在其他回調(diào)之后呵恢,進(jìn)而不會導(dǎo)致內(nèi)存泄露鞠值;
事件響應(yīng)/手勢識別
對于IOKit.framework
生成的IOHIDEvent
(如觸摸/鎖屏/靜音/傳感器加速等)會發(fā)送給SpringBoard
接收,并通過mach port
發(fā)送給注冊了相應(yīng)端口的source1
應(yīng)用進(jìn)程渗钉,進(jìn)而觸發(fā)事件回調(diào)__IOHIDEventSystemClientQueueCallback
彤恶,并通過_UIApplicationHandleEventQueue
內(nèi)部注冊source0
事件進(jìn)行事件應(yīng)用內(nèi)部分發(fā);
手勢識別就是將上面識別的手勢UIGestureRecognizer
標(biāo)記為待處理鳄橘,并注冊了observer
監(jiān)測BeforeWaiting
事件声离,觸發(fā)回調(diào)來處理待處理的手勢;
界面更新
蘋果注冊了observer
監(jiān)測BeforeWaiting/Eixt
事件挥唠,當(dāng)事件發(fā)生時會將已提交到全局容器待界面繪制的任務(wù)執(zhí)行并更新UI抵恋,如果中間執(zhí)行大量邏輯計算的任務(wù)導(dǎo)致runloop
遲遲不觸發(fā)ui更新的話,就會導(dǎo)致繪制ui的幀被丟棄即“丟幀”宝磨,進(jìn)而引發(fā)ui卡頓弧关,FaceBook
推出的開源項目AsyncDisplayKit 就是防止主線程存在大量與ui不相關(guān)的任務(wù)處理(通過后臺線程處理)阻塞ui更新,來避免“丟幀”提升界面流暢度唤锉;
GCD
對于提交至主隊列的任務(wù)世囊,如dispatch_source timer
、dispatch_async
窿祥,都是主隊列runloop
中監(jiān)聽相對應(yīng)的mach port
事件株憾,當(dāng)事件發(fā)生時(timer
到期或dispatch_async
添加到主隊列任務(wù)),libdispatch
就會通過mach port
端口向監(jiān)聽該端口的runloop
發(fā)送喚醒消息晒衩,被喚醒的runloop
觸發(fā)回調(diào)函數(shù)__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
嗤瞎,該回調(diào)函數(shù)內(nèi)部的_dispatch_main_queue_callback_4CF
實際是由libdispatch
定義處理的,即處理相應(yīng)的任務(wù)听系;
網(wǎng)絡(luò)請求
對于NSURLConnection
實現(xiàn)原理如下圖贝奇,具體為:
當(dāng)開始網(wǎng)絡(luò)傳輸時,我們可以看到 NSURLConnection 創(chuàng)建了兩個新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private靠胜。其中 CFSocket 線程是處理底層 socket 連接的掉瞳。NSURLConnectionLoader 這個線程內(nèi)部會使用 RunLoop 來接收底層 socket 的事件毕源,并通過之前添加的 Source0 通知到上層的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通過一些基于 mach port 的 Source 接收來自底層 CFSocket 的通知陕习。當(dāng)收到通知后霎褐,其會在合適的時機(jī)向 CFMultiplexerSource 等 Source0 發(fā)送通知,同時喚醒 Delegate 線程的 RunLoop 來讓其處理這些通知该镣。CFMultiplexerSource 會在 Delegate 線程的 RunLoop 對 Delegate 執(zhí)行實際的回調(diào)冻璃。
AFNetworking
使用的常駐線程用于后臺接收delegate
回調(diào),當(dāng)有任務(wù)需要處理時损合,通過performSelector:onThread:
將任務(wù)提交給該線程的runloop
來處理俱饿,具體后臺常駐線程創(chuàng)建如下:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
最重要的就是在runloop
中添加了的未接收任何消息的NSMachPort
進(jìn)去,防止runloop
退出進(jìn)而線程退出塌忽;
小知識
宏定義 do{ }while(0)
- 幫助定義復(fù)雜的宏以避免錯誤
#define DO_SOMETHING() foo1();foo2();
if (a>0)
DO_SOMETHING();
//展開后如下
if (a>0)
foo1();
foo2();
-
避免使用
goto
跳轉(zhuǎn)int foo() { if (error1) { do_something(); goto END: } if (error2) { do_something2(); goto END; } END: xxx; } //使用do{}while(0) int foo() { do { if (error1) do_something(); if (error2) do_something2(); } while(0) xxx; }
控制代碼塊
-
避免由空宏定義造成的警告
內(nèi)核中由于不同架構(gòu)的限制,很多時候會用到空宏失驶,土居。在編譯的時候,這些空宏會給出warning嬉探,為了避免這樣的warning擦耀,我們可以使用do{...}while(0)來定義空宏:
#define EMPTYMICRO do{}while(0)
; 這種情況不太常見涩堤,因為有很多編譯器眷蜓,已經(jīng)支持空宏。
CHECK_FOR_FORK()宏定義用途
主要對于非移動端平臺胎围,如Mac OSX吁系,進(jìn)程調(diào)用fork
生成子進(jìn)程,一般是直接調(diào)用exec
或類似的函數(shù)執(zhí)行新的程序白魂,而對于依賴Core Founadtion / Cocoa / Core Data 框架的應(yīng)用,必須調(diào)用 exec 函數(shù),否則這些框架也許不能正確的工作汽纤。
Warning: When launching separate processes using the fork function, you must always follow a call to fork with a call to exec or a similar function. Applications that depend on the Core Foundation, Cocoa, or Core Data frameworks (either explicitly or implicitly) must make a subsequent call to an exec function or those frameworks may behave improperly.
-- 摘自Threading Programming Guide
理解:應(yīng)該是避免進(jìn)程使用
vfork
系統(tǒng)調(diào)用繼續(xù)使用父進(jìn)程的數(shù)據(jù),導(dǎo)致影響父進(jìn)程福荸,因此要求立即調(diào)用exec
去執(zhí)行新的程序蕴坪;
Reference
Threading Programming Guide -- Runloop
demo
https://github.com/FengyunSky/notes/blob/master/local/code/runloop.tar