曾有一位做单片机开发的朋友问我回调是怎么回事,我解释就跟单片机的中断函数类似,比如你的定时器的溢出值设置为200毫秒,那么单片机内部电路每200毫秒,就会把PC设置成你的定时器中断向量入口,达到调用中断程序的目的。在浏览器上,JS引擎负责做这个事,回调函数就相当于ISR。
后来一想,这种说法有问题。只怪我当时对Javascript的特性不是很了解。表面上看它很恰当,但是却让人忽略了一些重要的问题。MCU的中断,是那种会直接打断程序流程的东西,而Javascript的回调,会很绅士地一直等下去。
MCU是怎样的?
还是拿「定时器中断」来说,当定时器的计数器溢出或达到阈值,就会改变内部一个标志位。正在运行的指令执行完后,这个中断标志就会被处理,不同的MCU会有不同的处理方式。
大致原则是:
- 除非正在处理更高优先级的中断,否则立即响应定时器中断,调用中断处理子程序。
- 当正处在其他不可打断的中断(可能是优先级高,也可能是CPU本来就不支持打断),那么就等着,那个一完,这个就立即响应。
也就是说,基本上MCU会立即响应!
单片机工程师在这一块会非常小心,举个例子,当MCU字长小于数据的字长,比如在8位机上使用int,如果这个值碰巧也会被ISR修改,就需要在处理int前,先关掉全局中断,处理完了重新打开。因为你不知道什么时候中断会产生并打断你。
出于类似的原因,软件开发人员编写多线程程序时,经常需要「加锁」。
Javascript是怎样的?
我们拿JS的setTimeOut来做个实验。(正好和定时器中断做个对比)
setTimeout(() => console.log("cool"), 1000)
这个会在1秒钟后,打印出一个“cool”。一切在预期中,与「MCU中断」行为类似。
举其他的例子之前,我们先定义一个函数,通过死循环来达到延时的效果。
function sleepSync(ms) {
var mark = new Date().getTime()
while (new Date().getTime() - mark < 4000)
}
再看另外一个例子
setTimeout(() => console.log("cool"), 1000)
sleepSync(4000)
出问题了,过了4秒之后才打印出“cool”。setTimeout并不能打断那个while
死循环。
用其他的Javascript函数做个实验
// let's try to read a non-existing file
fs.readFile("/tmp/blah", (e, _) => console.log(e.message))
sleepSync(4000)
同样是过了4秒,才打印出
ENOENT: no such file or directory, open '/tmp/blah'
所以JS的「回调」和「MCU的中断」,是不能等同的,它们有一些相似点,但是在关键的地方不太一样,所以以后还是不要打这个比方吧。
至于Javascript为什么是这个样子,这跟Javascript的Event Loop
和task queue
有关了,但那是另外一个主题(如果感兴趣,请看另一篇博客),这篇文章只是纠正一下过去自以为很棒的比喻。
原文:http://madmuggle.me/articles/JSCallbackIsNotMCUInterrupt.html