思考下面的两个场景
注:以下示例代码均用typescript实现
let i = 0;
async function handler() {
i++;
console.log(new Date().getTime()/1000, '处理程序开始', i);
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟异步操作
console.log(new Date().getTime()/1000, '处理程序完成', i);
};
场景一:直接调用handler两次
function call1() {
handler();
handler();
}
call1();
打印如下:
可以看出,两次handler都同时先后进入(由于js单线程,i会变化),并同时卡在await等待2s。1732934781.962 处理程序开始 1
1732934781.966 处理程序开始 2
1732934783.969 处理程序完成 2
1732934783.97 处理程序完成 2
场景二:await调用handler两次
async function call2() {
await handler();
await handler();
}
call2();
打印如下:
1732934929.563 处理程序开始 1
1732934931.569 处理程序完成 1
1732934931.57 处理程序开始 2
1732934933.571 处理程序完成 2
可以看出,下一次的handler会等上一次执行完毕再进行。
由此可以看出,js如果直接调用函数2次,函数中开头非异步的部分会按调用顺序执行,并同时卡在函数中异步的部分,异步结束后再执行剩余部分。
这很可能造成数据不同步等各种问题。对于要求不管是直接调用还是await调用多次,都要完全保证顺序的地方,需要引入锁机制。
核心就是,函数在进入时立即锁住,即便是直接调用多次,也会确定等待锁释放。
而根据前面的试验结论,可以用一个简单的异步promise来实现这个锁。
let lastlock = Promise.resolve();
async function getlock() {
let unlock = () => {};
let lock = lastlock;
let nlock = new Promise<void>((resolve) => {unlock = resolve;});
lastlock = nlock;
await lock;
return unlock;
}
上面的getlock函数,核心要点
1. lastlock一开始是一个已经resolve的promise,所以await它不会阻塞
2. 每次获取锁,js单线程执行会更改lastlock为一个新的promise,而这个promise的resolve函数会被传出来,也就成了一个unlock函数,这个unlock执行会resolve promise,完成异步。
改造handler函数,用锁
async function handler() {
const unlock = await getlock();
try {
i++;
console.log(new Date().getTime()/1000, '处理程序开始', i);
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟异步操作
console.log(new Date().getTime()/1000, '处理程序完成', i);
} finally {
unlock();
}
};
连续直接调用handler两次(即call1())
1732935948.492 处理程序开始 1
1732935950.497 处理程序完成 1
1732935950.497 处理程序开始 2
1732935952.499 处理程序完成 2
连续await调用handler两次(即call2()),打印如下
1732936175.165 处理程序开始 1
1732936177.171 处理程序完成 1
1732936177.171 处理程序开始 2
1732936179.173 处理程序完成 2