事件循环——不一样的死循环
一谈起死循环,我们首先想到的就是程序假死、机子发热等,唯恐避之不及。但其实程序的世界是离不开死循环的,无论是运行在手机端的app,还是运行在服务器上的服务,都需要死循环来让程序一直执行不退出。那么,这类死循环和造成机器假死的死循环有什么不一样呢?
万恶的死循环?
说起死循环,首先想到的就是下面的这一段代码。
while (YES) {
NSLog(@"test infinite loop");
}
让这段代码在模拟器跑起来,不一会儿我们就能听见风扇呼呼的转个不停,通过Xcode查看CPU占用率。
死循环直接将主线程的CPU占满了,因为iPhone是多核的,所以CPU的使用率并没有到达100%。
那么,是不是说死循环就一定会导致CPU满负荷运转,造成手机发热,耗电增加呢?
我们来看这段代码。
while (YES) {
sleep(1); // 每次循环睡1s
NSLog(@"test infinite loop");
}
这回CPU的使用率是怎么样的呢?
哇塞!CPU消耗竟然几乎可以忽略了!sleep到底干了什么呢?查查wiki,它是这么说的。
Sleep让线程或者进程放弃剩余的时间片,在指定的时间内保持
Not Runnable
状态。一旦指定的时间过去之后,系统会通过信号中断等方式来唤醒该线程或者进程。
也就是说,线程或者进程进入Not Runnable
的状态的时候,对系统资源的消耗是非常少的。而且在这样的状态下,系统提供了信号、中断等方式来唤醒线程或进程。
这样的死循环绝对不会是什么坏事啊!那程序的世界里面有没有此类死循环的使用场景呢?必然是有的。
事件循环
没错!我要说的就是事件循环 —— Event Loop。
好吧,wikipedia告诉我们,这个东西的名儿挺多。
event loop, message dispatcher, message loop, message pump, or run loop
虽然本文要开刀的是RunLoop,但其实无论是客户端还是服务端需要事件循环,其机制也是大同小异的。 比如NodeJS就需要事件循环来实现非阻塞的IO调用,毕竟JS是单线程的语言,否则的话,它怎么撑起JS服务器一片天呢?
当我们打开一个应用后,这个应用就开始运行,只要这个应用在前台(后台待久了会被系统强制杀掉),它就不会自己退出,会一直显示着,而且随时能响应我们的操作。最最重要的是,当我们不操作的时候,手机一般是不会发烫的,也就是CPU使用率几乎是0。
对,这么牛逼的特性就是抱的RunLoop的大腿,RunLoop是苹果实现的事件循环。
RunLoop
不多说,直接看看这个循环在哪里?代码源自苹果开源的CoreFoundation。
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
只要不是Stopped或者Finished的状态,runloop就会跑啊跑,不停歇。那为什么这个循环会不消耗CPU呢?原理无非跟上面咱们说的sleep是一样的,看代码验证一下。
在CFRunLoopRunSpecific
里面调用了__CFRunLoopServiceMachPort
。
static Boolean __CFRunLoopServiceMachPort(......) {
......
for (;;) {
......
ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
......
if (MACH_MSG_SUCCESS == ret) {
......
return true;
}
if (MACH_RCV_TIMED_OUT == ret) {
......
return false;
}
......
}
HALT;
return false;
}
在这个函数里面看到了一个系统函数mach_msg
,只有在ret是MACH_MSG_SUCCESS
或者MACH_RCV_TIMED_OUT
的时候,才能跳出死循环。
查阅mach_msg的相关文档,他是这么说的:
System Trap / Function - Send and/or receive a message from the target port.
里面有个参数需要关注下
timeout:
[in scalar] When using the MACH_SEND_TIMEOUT and MACH_RCV_TIMEOUT options, specifies the time in milliseconds to wait before giving up. Otherwise MACH_MSG_TIMEOUT_NONE should be supplied.
CFRunLoopRunSpecific
传递进来的timeout参数要么是0,要么是无穷大,但这些都不重要,重要的是这里的wait。
调用mach_msg
这个函数,会将线程挂起,然后等待指定的时间,如果期间从目标端口有消息收到,系统就会唤醒该线程,继续执行逻辑,这跟sleep是异曲同工的。
这也就是RunLoop这个死循环不会导致CPU使用率爆表的本质原因。
牛逼的依然是底层
事件循环就这么简单,主要是靠系统的消息机制、中断机制实现了线程状态的切换,节省了不必要的CPU消耗。关于中断和消息的详细介绍,还是老老实实拿本计算机体系结构的书啃一啃吧。