当异步抽象数据类型需要等待资源时,竞态条件和死锁就会悄悄潜入。互斥保证某段代码同一时刻只有一个计算在执行,是守护共享状态的关键机制。
设想一个图书馆系统,支持 checkout(借书)和 checkin(还书)。当一本书被借走时,后来的读者必须等待它归还。这个场景把异步等待、竞态和互斥都集中到了一起。
最朴素的实现是同步抽象数据类型:checkout 要么立即成功,要么立即失败。它简单直接,但无法表达「书被借走时我愿意等」的需求。
把 checkout 改成异步的:当书不可用时,它返回一个 Promise<void>,等书被归还时才完成。这让调用者不必阻塞,而是可以继续做别的事。
问题来了:checkout 需要返回一个 Promise,但此刻还没有书可以归还——Promise 要在未来某个时刻才被解决。为此我们需要一种「先创建、后解决」的工具。
Deferred<T>:一个辅助 ADT,把 Promise 的创建与解决分离。它暴露promise(给客户端)和resolve/reject(给未来触发者)。
现代 JavaScript 可以用 Promise.withResolvers() 直接获得一个 Deferred:
const { promise, resolve, reject } = Promise.withResolvers<void>();
// promise 返回给等待的读者,resolve 在书归还时调用
图书馆为每本被借出的书维护一个等待队列。checkout 把一个 Deferred 推入队列;checkin 归还书时,从队列取出 Deferred 并 resolve 它。
class Library {
private holds: Map<string, Deferred<void>[]> = new Map();
async checkout(book: string): Promise<void> {
if (this.isAvailable(book)) {
this.markBorrowed(book);
return; // 立即成功
}
const { promise, resolve } = Promise.withResolvers<void>();
this.holds.get(book)!.push({ promise, resolve });
return promise; // 等待归还
}
checkin(book: string): void {
const queue = this.holds.get(book)!;
const next = queue.shift();
if (next) next.resolve(); // 唤醒等待者
else this.markAvailable(book); // 书重新可借
}
}
当读者想借多本书时,一个微妙的竞态出现了:如果先等第一本书、拿到后再借第二本,而另一个读者恰好在这时借走了第二本书,双方可能陷入互相等待。这就是「持有并等待」导致的竞态。
修复思路是尽快借到每一本书:不要等前一本借到再去借下一本,而是为每本书独立排队,一旦某本就绪就立即锁定它。
死锁(deadlock):并发模块互相等待对方持有的资源,导致所有相关方都无法继续执行。
死锁通常需要四个条件同时成立:互斥、持有并等待、不可抢占、循环等待。破坏其中任意一个,就能预防死锁。
在 TypeScript/JavaScript 中,await 之间的代码是连续执行的,不会被其他任务交错。这给了我们一个天然的互斥区域:只要把关键操作放在同一个 await 区间内,它们就等价于原子执行。
互斥(mutual exclusion):保证某段区域同一时刻只有一个计算在执行。
利用这一性质,我们可以先通过 await 排队等待每本书就绪,然后在一个无 await 的临界区里一次性借出所有书。这样既避免了竞态,又不会死锁。
互斥是一个普适问题,不同层次有不同的解法:
await 之间的代码段天然实现互斥。