Event Loop
Event Loop
事件循环机制,如javascript
的事件循环,以及依赖其的nodejs
都是采用的异步事件循环机制。
对于上述两者,都是基于多线程,但是都是单线程执行任务代码,其依赖的就是Event Loop
事件循环机制,通过事件队列注册事件及事件的观察者,事件的执行交由其他线程去执行(如I/O操作,网络请求等),nodejs
采用的是libuv
异步I/O线程池库;对于非异步I/O操作,如setTimeOut
setInterval
等,都是基于事件循环查询(每次事件处理完成后进入下一次事件循环时都会查看时间是否已到达,并且是任务是插入到任务队列尾部,因此存在误差,不过也可采用process.netxTick
会将事件插入到事件循环前解析执行,且可嵌套执行);
- 定时器:本阶段执行已经被
setTimeout()
和setInterval()
的调度回调函数。- 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
- idle, prepare:仅系统内部使用。
- 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和
setImmediate()
调度的之外),其余情况 node 将在适当的时候在此阻塞。- 检测:
setImmediate()
回调函数在这里执行。- 关闭的回调函数:一些关闭的回调函数,如:
socket.on('close', ...)
。
JavaScript 运行机制详解:再谈Event Loop
Mach Port
这个在《unix进程间通信》中已阐述,在这不过多阐述;
RunLoop
RunLoop
就像其名字一样,就是运行循环,核心就是事件循环+mach port,利用事件循环注册观察相应的事件,若无事件处理,线程就去睡眠等待内核事件触发或者通过手动唤醒,不停地循环处理各种事件(如timer
source0
source1
事件以及dispatch
分发的func block
等);
注:以下代码分析基于CF-1151.16源码;
runloop与线程的关系
直接上结论:runloop
与线程是一一对应的,且对于主线程是默认开启的,对于其他线程,需要通过手动开启,且只能通过苹果对外的接口获取线程相应的CFRunLoopRef
对象:
CFRunLoopRef CFRunLoopGetMain(void);
CFRunLoopRef CFRunLoopGetCurrent(void);
原理就是:苹果维护了一个全局的字典对象,若字典中不存在线程对应的runloop
对象就会创建并赋值,并且还利用线程私有数据(数组)存储了指定__CFTSDKeyRunLoop
当前线程的CFRunLoopRef
对象(同时也关联了runloop
对象销毁的回调,用于线程退出销毁);策略是:优先从线程私有数据数组中获取,若获取不到就从全局字典对象中获取,若无则去创建;
runloop对象结构分析
CoreFoundation
中关于runloop
的五个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中CFRunLoopRef
对象结构体如下:
//CFRunLoop结构体结构
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;//当前mode
CFMutableSetRef _modes;//所有的mode
struct _block_item *_blocks_head;//添加的block任务,与dispatch分发的block处理不同
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
runloop
对象的结构体为__CFRunLoop
,其存储着runloop
相关的锁保证线程安全(注意下NSRunLoop不是线程安全的),唤醒端口(用于CFRunLoopWakeUp
外部接口调用,主要是source0
),绑定的线程,mode
各种集合(下面会重点阐述),block
处理任务(通过CFRunLoopPerformBlock
接口注册的)以及记录需要的相关信息(如运行时间_runTime
_sleepTime
)等;
CFRunLoopModeRef
CFRunLoopRef
对象中包含了若干Mode
,Mode
对象的数据结构如下:
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;//需要监听的所有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实现<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
相关的Mode
,通过CFRunLoopAddCommonMode
添加Mode
;
runloop
可添加多个Mode
,但只能指定一个Mode
模式运行(默认是kCFRunLoopDefaultMode
),且需要退出当前重新指定运行才能生效;每个Mode
中可添加若干source
、timer
、observer
;
对于CoreFoundation
中的CFRunLoop
苹果只提供了两种默认模式kCFRunLoopDefaultMode
和kCFRunLoopCommonModes
,其中kCFRunLoopCommonModes
只是操作common
标记的字符串,用于向所有现有的Modes
中添加相应的观察者,不是一种具体的Mode
,不能直接用于CFRunLoopRunInMode
调用运行;但上层NSRunLoopMode封装了一些相应的Mode
,如
-
NSDefaultRunLoopMode
即kCFRunLoopDefaultMode
默认的模式 -
NSEventTrackingRunLoopMode
模态跟踪事件时,例如鼠标拖动循环,应将运行循环设置为此模式; -
NSModalPanelRunLoopMode
运行等待模态窗口(如NSSavePanel
NSOpenPanel
)的输入时指定; -
UITrackingRunLoopMode
运行控件追踪时指定,如UIScrollView
滑动时(这个系统会默认自动切换到此模式)
iOS
应用启动时系统默认注册了5个Mode:
-
kCFRunLoopDefaultMode
: App的默认 Mode,通常主线程是在这个Mode
下运行的; -
UITrackingRunLoopMode
: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响; -
UIInitializationRunLoopMode
: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用; -
GSEventReceiveRunLoopMode
: 接受系统事件的内部 Mode,通常用不到; -
kCFRunLoopCommonModes
: 这是一个占位的 Mode,没有实际作用;
CFRunLoopSourceRef
CFRunLoopSourceRef
对象的数据结构如下:
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;
其中联合体union
中的versionx.version
字段信息用于区分source0
或者source1
;
-
source0
,结构体中context
上下文中包含了各种回调函数,主要是perform
回调函数(用于执行添加到source0
中的任务),当调用CFRunLoopSourceSignal
时会标记__CFRunLoopSource
中的_bits
标记位,然后调用CFRunLoopWakeUp
来唤醒runloop
再下一个循环中处理此回调;主要用于APP内部事件,由APP负责管理触发,如
UIEvent
事件; source1
,不同于source0
执行回调函数,source1
还需要指定mach port
,用于监听系统内核事件或其他线程发来的事件;
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
结构中包含了时间相关的变量,runloop
被timer
事件触发都会去检查当前所有的timer
时间点是否达到,若达到则处理事件任务;具体触发时间事件主要包含两种mk_timer
及dispatch source
形式,两者都是基于mach port
但是触发runloop
并处理回调的处理方式不同;
mk_timer
是通过__CFRunLoopDoTimers
来处理,依赖于runloop
来触发时间回调函数,因此基于此的NStimer
及performSelector:withObject:afterDelay:
(是对NSTimer
的包装),都需要runloop
运行;
而dispatch source
(针对主队列,其他队列不是通过runloop来触发)是通过__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
中指定的外部函数_dispatch_main_queue_callback_4CF
来处理,调用堆栈如下
dispatch_after
指定主队列的时间任务,是对dispatch_source
的包装,对于存在UI的应用默认主队列的runloop
是开启的,若是其他工具类应用,则需要手动开启主队列runloop
,否则指定主队列的dispatch source
是无法生效的;
Timer有两种实现方式分别是MK_Timer和GCD Timer,在runloop中Timer被转为了一个存了触发时间的列表,这个触发时间是一个绝对时间,会按时间大小升序排序,在最小的时间被触发后,Runloop会更新列表保证时间始终是升序排列。如果Runloop在某次运行中阻塞了很长时间,Timer的触发会受到影响。过期的时间点会被移除而不会去触发。
具体的NSTimer
与GCD Timer
实现剖析可参考从NSTimer的失效性谈起(二):关于GCD Timer和libdispatch
不过在源码中的USE_DISPATCH_SOURCE_FOR_TIMERS
未生效,暂时未搞清问题,待后续补充;
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
相应状态变化(_activities
指定需要观察的类型)及状态变化的回调指针_callout
,具体观察的选项包括:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
RunLoop事件循环
runloop
事件循环的逻辑如上图,对于黄色的Block
是通过CFRunLoopPerformBlock
添加的block
任务,若runloop
任务处理完成后就会休眠等待source1
、timer
、或者手动被唤醒来继续下一次循环处理任务;
RunLoop实践
AutoReleasePool
主要是利用runloop observer
观察注册的事件:kCFRunLoopEntry
、kCFRunLoopBeforeWaiting
、kCFRunLoopExit
,分别用于autoreleasepool
push/pop
操作来创建/释放内存池,并且保证自动内存池创建优先级其他回调之前,释放内存池在其他回调之后,进而不会导致内存泄露;
事件响应/手势识别
对于IOKit.framework
生成的IOHIDEvent
(如触摸/锁屏/静音/传感器加速等)会发送给SpringBoard
接收,并通过mach port
发送给注册了相应端口的source1
应用进程,进而触发事件回调__IOHIDEventSystemClientQueueCallback
,并通过_UIApplicationHandleEventQueue
内部注册source0
事件进行事件应用内部分发;
手势识别就是将上面识别的手势UIGestureRecognizer
标记为待处理,并注册了observer
监测BeforeWaiting
事件,触发回调来处理待处理的手势;
界面更新
苹果注册了observer
监测BeforeWaiting/Eixt
事件,当事件发生时会将已提交到全局容器待界面绘制的任务执行并更新UI,如果中间执行大量逻辑计算的任务导致runloop
迟迟不触发ui更新的话,就会导致绘制ui的帧被丢弃即“丢帧”,进而引发ui卡顿,FaceBook
推出的开源项目AsyncDisplayKit 就是防止主线程存在大量与ui不相关的任务处理(通过后台线程处理)阻塞ui更新,来避免“丢帧”提升界面流畅度;
GCD
对于提交至主队列的任务,如dispatch_source timer
、dispatch_async
,都是主队列runloop
中监听相对应的mach port
事件,当事件发生时(timer
到期或dispatch_async
添加到主队列任务),libdispatch
就会通过mach port
端口向监听该端口的runloop
发送唤醒消息,被唤醒的runloop
触发回调函数__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
,该回调函数内部的_dispatch_main_queue_callback_4CF
实际是由libdispatch
定义处理的,即处理相应的任务;
网络请求
对于NSURLConnection
实现原理如下图,具体为:
当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。
AFNetworking
使用的常驻线程用于后台接收delegate
回调,当有任务需要处理时,通过performSelector:onThread:
将任务提交给该线程的runloop
来处理,具体后台常驻线程创建如下:
+ (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
进去,防止runloop
退出进而线程退出;
小知识
宏定义 do{ }while(0)
- 帮助定义复杂的宏以避免错误
#define DO_SOMETHING() foo1();foo2();
if (a>0)
DO_SOMETHING();
//展开后如下
if (a>0)
foo1();
foo2();
-
避免使用
goto
跳转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; }
控制代码块
-
避免由空宏定义造成的警告
内核中由于不同架构的限制,很多时候会用到空宏,。在编译的时候,这些空宏会给出warning,为了避免这样的warning,我们可以使用do{...}while(0)来定义空宏:
#define EMPTYMICRO do{}while(0)
; 这种情况不太常见,因为有很多编译器,已经支持空宏。
CHECK_FOR_FORK()宏定义用途
主要对于非移动端平台,如Mac OSX,进程调用fork
生成子进程,一般是直接调用exec
或类似的函数执行新的程序,而对于依赖Core Founadtion / Cocoa / Core Data 框架的应用,必须调用 exec 函数,否则这些框架也许不能正确的工作。
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
理解:应该是避免进程使用
vfork
系统调用继续使用父进程的数据,导致影响父进程,因此要求立即调用exec
去执行新的程序;
Reference
Threading Programming Guide -- Runloop
demo
https://github.com/FengyunSky/notes/blob/master/local/code/runloop.tar