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消耗。关于中断和消息的详细介绍,还是老老实实拿本计算机体系结构的书啃一啃吧。
infinite loop
busy waiting
sleep
event loop
The Node.js Event Loop, Timers, and process.nextTick()
mach_msg