这更多的是一个假设性问题。有时 JavaScript 代码可能会进入无限循环并阻塞一切,特别是在开发的早期阶段。我知道最好的解决方案是编码,这样这种情况就不会出现,但如果在某种情况下不可能编写完整的代码怎么办(可能是因为我们无法控制输入或其他东西)。
有没有什么方法可以让我们以编程方式确定代码何时处于无限循环并终止它?
也许在单独的线程或子进程中同步运行可能发生这种情况的代码片段,并确定该线程/进程何时处于无限循环中。
如何才能做到这一点呢?是否可以通过单独确定某个线程(主线程或子线程)的 CPU 使用率来确定该线程何时处于无限循环中?
根据下面评论中的讨论,我意识到仅凭跟踪类似语句的重复执行和高资源利用率来识别阻塞无限循环是不可能的。所以我认为解决方案在于两者的结合。但这又回到了我们之前的问题。我们如何通过脚本来测量CPU使用率?
对于简单的循环,您可以使用此处使用的方法(pdf链接)。它使用基于路径的方法来检测无限循环。语句被视为图的节点。
您还可以看到这篇论文。他们已经为java程序实现了运行时解决方案。
鉴于没有提出其他可行/复杂的解决方案,低技术解决方案是仅使用计数器,并在达到荒谬的高数字时中止。
这需要手动编码,但当您知道无限循环已经发生时,这对于调试无限循环很有用。
var count = 0
while(true){
if(count++>100000){
//assume infinite loop, exit to avoid locking up browser while debugging
break;
}
}
最有效的检测无限循环的方法是进行测试来运行代码并告诉您是否有无限循环:使用测试框架(这在 2014 年是正确的,在 2024 年仍然是正确的,它毫无疑问,到 2034 年及以后仍然如此 =)
所有这些都允许单个测试仅运行几秒钟,因此,如果您的代码(部分)有无限循环(或者某些性能很差以至于“感觉”像一个,即使事实并非如此),那么该测试将会超时并被计为测试错误供您查看和修复。
取而代之的是,一种非常简单但 95%“足以帮助您防止它们”的方法是告诉您的 linter 标记 while
for
循环(迭代之间运行的东西),因为这些是无限循环错误的主要原因。
这不会捕获无限递归(但是没关系,无限递归不会锁定线程,它只是运行几秒钟,然后在运行时耗尽调用堆栈),并且它不会捕获导致的无限循环通过像
for (let i=100; i>=0; i++)
这样的拼写错误,但它会
告诉你,有人编写的代码很可能有错误,无论是现在还是将来当其他人修改循环体时。最后,您还可以使用预处理器重写所有循环,以在开发模式下运行时包含中断计数器(例如,您可能已经在使用转译器,因为您正在编写打字稿,或者使用 esbuild 捆绑代码或 webpack 等),这样就可以:
for (...) {
// ...
}
while (...) {
// ...
}
do {
// ...
} while (...)
(() => {
// Infinite Loop Detection Wrapper
let __break_counter = 0;
for (...) {
if (__break_counter++ > 1000) {
throw new Error(`...`);
}
// ...
}
})();
(() => {
// Infinite Loop Detection Wrapper
let __break_counter = 0;
while (...) {
if (__break_counter++ > 1000) {
throw new Error(`...`);
}
// ...
}
})();
(() => {
// Infinite Loop Detection Wrapper
let __break_counter = 0;
do {
if (__break_counter++ > 1000) {
throw new Error(`...`);
}
// ...
} while(...)
})();
当您使用浏览器内“编辑器”时,这往往是您唯一的选择,即页面的某些页面允许人们编写代码,然后您的页面在 iframe、画布等上预览/呈现。您没有可用的源代码构建系统和 linting 工具。例如,以下是一个用 vanilla JS 编写的简单循环保护,它可以插入几乎任何东西:
const errorMessage = `Potentially infinite loop detected.`;
const loopStart = /\b(for|while)[\r\n\s]*\([^\)]+\)[\r\n\s]*{/;
const doLoopStart = /\bdo[\r\n\s]*{/;
const doLoopChunk =
/}(\s*(\/\/)[^\n\r]*[\r\n])?[\r\n\s]*while[\r\n\s]*\([^\)]+\)([\r\n\s]*;)?/;
/**
* Walk through a string of source code, and wrap all `for`, `while`, and
* `do...while` in IIFE that count the number of iterations and throw if
* that number gets too high.
*
* Note that this uses an ESM export, but you can trivially rewrite this
* to `module.exports = loopGuard(...)` if you need legacy CJS, or
* `window.loopGuard = function loopGuard(...)` if you need a browser
* global (but neither should be necessary anymore at time of posting).
*
* @param {String} code The source code to "safe-i-fy".
* @param {number?} loopLimit The iteration count threshold for throwing.
* @param {number?} blockLimit The replacement count threshold, after which this function decides it's probably in an infinite loop, itself.
* @returns {String} The code with all loop blocks wrapped in safety IIFE.
*/
export function loopGuard(code, loopLimit = 1000, blockLimit = 1000) {
let ptr = 0;
let iterations = 0;
while (ptr < code.length) {
if (iterations++ > blockLimit) throw new Error(errorMessage);
const codeLength = code.length;
let block = ``;
let loop = ptr + code.substring(ptr).search(loopStart);
let doLoop = ptr + code.substring(ptr).search(doLoopStart);
// do the numbers make sense?
if (loop < ptr) loop = codeLength;
if (doLoop < ptr) doLoop = codeLength;
if (loop === codeLength && doLoop === codeLength) return code;
// if we ge there, we have a source block to extract:
let nextPtr = -1;
if (loop < codeLength && loop <= doLoop) {
nextPtr = loop;
block = getLoopBlock(code, loop);
} else if (doLoop < codeLength) {
nextPtr = doLoop;
block = getDoLoopBlock(code, doLoop);
}
// Quick sanity check:
if (block === `` || nextPtr === -1) return code;
// Replace the block's code and increment the pointer so that it points
// to just after the IIFE's throw instruction. Note that we don't increment
// it by wrapped.length, because the code we just wrapped might have nested
// loops, and we don't want to skip those.
const wrapped = wrap(block, loopLimit);
code = code.substring(0, ptr) + code.substring(ptr).replace(block, wrapped);
ptr = nextPtr + wrapped.indexOf(errorMessage) + errorMessage.length + 6;
}
return code;
}
/**
* In order to find the full for/while block, we need to start at
* [position], and then start tracking curly braces until we're back
* at depth = 0.
*/
function getLoopBlock(code, position = 0) {
let char = ``;
let depth = 1;
let pos = code.indexOf(`{`, position) + 1;
while (depth > 0 && position < code.length) {
char = code[pos];
if (char === `{`) depth++;
else if (char === `}`) depth--;
pos++;
}
if (pos >= code.length) {
throw new Error(`Parse error: source code end prematurely.`);
}
return code.substring(position, pos);
}
/**
* Extract everything from [position] up to and including the final
* "while (...)". Note that we do not allow comments between the do's
* body and the while conditional. That is: you can do it, but then
* we won't guard your loop.
*/
function getDoLoopBlock(code, position = 0) {
const chunk = code.substring(position);
const block = chunk.match(doLoopChunk)[0];
const end = chunk.indexOf(block) + block.length;
return chunk.substring(0, end);
}
/**
* wrap a block of code in the break counter IIFE, breaking out of the
* loop by way of a throw if more than [loopLimit] iterations occur.
*/
function wrap(block, loopLimit = 1000) {
return `((__break__counter=0) => {
${block.replace(
`{`,
`{
if (__break__counter++ > ${loopLimit}) {
throw new Error("${errorMessage}");
}`
)}
})();`;
}
基本上可以归结为“你无法判断
任意代码是否会停止,但你
可以判断特定的循环结构是否可能永远不会停止”,并且你绝对设置警告工具向您介绍无限循环的可能性(此外,当您使用很可能导致无限循环错误的模式时警告您)。 您通常不需要知道是否存在存在
无限循环,您只需要知道是否可能会存在无限循环,这样您就可以防御性地编写代码。