一谈起死循环,我们首先想到的就是程序假死、机子发热等,唯恐避之不及。但其实程序的世界是离不开死循环的,无论是运行在手机端的app,还是运行在服务器上的服务,都需要死循环来让程序一直执行不退出。那么,这类死循环和造成机器假死的死循环有什么不一样呢?

万恶的死循环?

说起死循环,首先想到的就是下面的这一段代码。

while (YES) {
    NSLog(@"test infinite loop");
}

让这段代码在模拟器跑起来,不一会儿我们就能听见风扇呼呼的转个不停,通过Xcode查看CPU占用率。

死循环CPU率

死循环直接将主线程的CPU占满了,因为iPhone是多核的,所以CPU的使用率并没有到达100%。

那么,是不是说死循环就一定会导致CPU满负荷运转,造成手机发热,耗电增加呢?

我们来看这段代码。

while (YES) {
    sleep(1); // 每次循环睡1s
    NSLog(@"test infinite loop");
}

这回CPU的使用率是怎么样的呢?

带sleep的死循环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消耗。关于中断和消息的详细介绍,还是老老实实拿本计算机体系结构的书啃一啃吧。

参考文献