hry*_*rbn 27 javascript asynchronous event-loop async-await
我知道 JavaScript 是单线程的,从技术上讲,它可以\xe2\x80\x99t 具有竞争条件,但由于异步和事件循环,它可能具有一些不确定性。这里\xe2\x80\x99s是一个过于简单的例子:
\nclass TestClass {\n // ...\n\n async a(returnsValue) {\n this.value = await returnsValue()\n }\n b() {\n this.value.mutatingMethod()\n return this.value\n }\n async c(val) {\n await this.a(val)\n // do more stuff\n await otherFunction(this.b())\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n假设b()依赖于this.value自调用 以来没有发生更改a(),并且c(val)从程序中的多个不同位置快速连续地多次调用。这是否会造成数据竞争,导致和 的this.value调用之间发生变化?a()b()
作为参考,我已经使用mutex预先解决了我的问题,但我\xe2\x80\x99一直在质疑是否存在问题。
\nfre*_*ish 39
是的,竞争条件在 JS 中也可能并且确实发生。仅仅因为它是单线程的,并不意味着竞争条件不会发生(尽管它们比较罕见)。JavaScript 确实是单线程的,但它也是异步的:指令的逻辑序列通常被分为在不同时间执行的较小块。这使得交错成为可能,因此出现竞争条件。
对于这个简单的例子,考虑...
var x = 1;
async function foo() {
var y = x;
await delay(100); // whatever async here
x = y+1;
}
Run Code Online (Sandbox Code Playgroud)
...这是适应 JavaScript 异步世界的非原子增量的经典示例。
现在比较以下“并行”执行:
await Promise.all([foo(), foo(), foo()]);
console.log(x); // prints 2
Run Code Online (Sandbox Code Playgroud)
...与“顺序”一个:
await foo();
await foo();
await foo();
console.log(x); // prints 4
Run Code Online (Sandbox Code Playgroud)
请注意,结果不同,即foo()不是“异步安全”。
即使在 JS 中,有时也必须使用“异步互斥体”。您的示例可能是其中一种情况,具体取决于其间发生的情况(例如,如果发生某些异步调用)。如果没有异步调用,do more stuff看起来突变发生在单个代码块中(受异步调用限制,但内部没有异步调用允许交错),我认为应该没问题。请注意,在您的示例中, in 的赋值a是在等待之后,而 whileb在最终等待之前调用。
扩展@freakish的答案中的示例代码,可以通过实现异步互斥体来解决此类竞争条件。下面是一个我决定命名为 的函数的演示using,其灵感来自于C# 的using语句语法:
const lock = new WeakMap();
async function using(resource, then) {
while (lock.has(resource)) {
try {
await lock.get(resource);
} catch {}
}
const promise = Promise.resolve(then(resource));
lock.set(resource, promise);
try {
return await promise;
} finally {
lock.delete(resource);
}
}
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
let x = 1;
const mutex = {};
async function foo() {
await delay(500);
await using(mutex, async () => {
let y = x;
await delay(500);
x = y + 1;
});
await delay(500);
}
async function main() {
console.log(`initial x = ${x}`);
await Promise.all([foo(), foo(), foo()]);
console.log(`final x = ${x}`);
}
main();Run Code Online (Sandbox Code Playgroud)
const lock = new WeakMap();
async function using(resource, then) {
while (lock.has(resource)) {
try {
await lock.get(resource);
} catch {}
}
const promise = Promise.resolve(then(resource));
lock.set(resource, promise);
try {
return await promise;
} finally {
lock.delete(resource);
}
}
Run Code Online (Sandbox Code Playgroud)
using当上下文获取资源时,将 apromise与 a关联起来,然后在由于资源随后被上下文释放而解析时删除该承诺。resource其余的并发上下文将在每次关联的 Promise 解析时尝试获取资源。第一个上下文将成功获取资源,因为它会观察到lock.has(resource)is false。其余的将在第一个上下文获取它之后观察到,并等待新的承诺,重复循环lock.has(resource)。true
let x = 1;
const mutex = {};
Run Code Online (Sandbox Code Playgroud)
这里,创建一个空对象作为指定对象,mutex因为x它是一个基元,这使得它与任何其他恰好绑定相同值的变量无法区分。“使用”没有意义1,因为它1不是指绑定,它只是一个值。不过,“使用”确实有意义x,因此为了表达这一点,mutex使用它时要理解它代表的所有权x。这就是为什么lockis a WeakMap—— 它可以防止原始值意外地被用作互斥体。
async function foo() {
await delay(500);
await using(mutex, async () => {
let y = x;
await delay(500);
x = y + 1;
});
await delay(500);
}
Run Code Online (Sandbox Code Playgroud)
在本例中,仅将实际递增的 0.5s 时间片x设为互斥,这可以通过上面演示中两个打印输出之间大约 2.5s 的时间差来确认。递增保证x是原子操作,因为该部分是互斥的。
async function main() {
console.log(`initial x = ${x}`);
await Promise.all([foo(), foo(), foo()]);
console.log(`final x = ${x}`);
}
main();
Run Code Online (Sandbox Code Playgroud)
如果每个都foo()完全并发运行,时间差将为 1.5 秒,但由于 3 个并发调用之间的 0.5 秒是互斥的,因此额外的 2 个调用又引入了 1 秒的延迟,总共 2.5 秒。
为了完整起见,这里是不使用互斥体的基线示例,它演示了非原子递增的失败x:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
let x = 1;
// const mutex = {};
main();
async function foo() {
await delay(500);
// await using(mutex, async () => {
let y = x;
await delay(500);
x = y + 1;
// });
await delay(500);
}
async function main() {
console.log(`initial x = ${x}`);
await Promise.all([foo(), foo(), foo()]);
console.log(`final x = ${x}`);
}Run Code Online (Sandbox Code Playgroud)
x请注意,总时间为 1.5 秒,并且由于删除互斥锁引入的竞争条件,最终值不正确。