异步函数共享变量的splice操作:竞态场景下是否触发错误?
异步场景下splice与read操作的冲突问题解答
你担心的这个情况确实会发生,而且这完全是合理的顾虑——这不是你对异步执行机制的误解,而是JavaScript单线程异步模型下很容易出现的并发修改问题。
为什么会出现这个错误?
JavaScript是单线程语言,所有同步代码会一次性执行完毕,不会被打断。但异步任务(比如定时器回调、API响应回调、Promise.then回调等)会被放到任务队列中,等主线程空闲时才会依次执行。
如果readObject和spliceObject是被异步触发的(比如分别在两个setTimeout里,或者两个独立的用户操作回调),它们的执行时机由事件循环调度,完全可能出现这样的时序:
readObject执行到line4,找到了目标元素的index- 主线程切换,执行
spliceObject的代码,line10把目标元素从sharedObject中删除 - 主线程切回
readObject,执行line5时,sharedObject[index]已经是undefined,访问.value.varB自然会抛出TypeError: Cannot read properties of undefined (reading 'value')
我们可以用一段代码模拟这个场景:
const sharedObject = [{key: 'foo', value: {varA: 1, varB: 5}}]; function findIndex(arr, key) { return arr.findIndex(item => item.key === key); } function readObject(key) { console.log('read: 已找到目标元素的index'); const index = findIndex(sharedObject, key); // line4 // 主动让出主线程,模拟被异步任务打断 setTimeout(() => { const b = sharedObject[index].value.varB; // line5 console.log('read: 获取到varB:', b); }, 0); } function spliceObject(key) { console.log('splice: 执行删除操作'); const index = findIndex(sharedObject, key); sharedObject.splice(index, 1); // line10 } // 异步触发两个函数 setTimeout(() => readObject('foo'), 0); setTimeout(() => spliceObject('foo'), 0);
运行这段代码,你会看到控制台抛出预期的错误。
如何避免这个问题?
这里有几种常见的解决方案,根据你的场景选择:
- 原子化关键操作:把查找和访问的逻辑变成不可打断的同步块。在
readObject中,找到index后立即取出目标对象,后续操作基于这个变量(因为同步代码不会被打断,取出对象的操作是原子的):
function readObject(key) { const index = findIndex(sharedObject, key); // 先取出目标对象,后续操作基于这个变量 const targetItem = sharedObject[index]; if (targetItem) { // 加个存在性判断更安全 const b = targetItem.value.varB; // 后续逻辑 } }
即使之后spliceObject删除了数组中的元素,targetItem依然指向原来的对象实例,不会影响属性访问。
- 加锁机制:如果操作逻辑更复杂,可以用一个锁变量确保同一时间只有一个函数在操作
sharedObject:
let isOperationLocked = false; async function readObject(key) { // 等待锁释放 while (isOperationLocked) { await new Promise(resolve => setTimeout(resolve, 10)); } isOperationLocked = true; try { const index = findIndex(sharedObject, key); const b = sharedObject[index]?.value.varB; // 后续逻辑 } finally { // 无论成功失败都释放锁 isOperationLocked = false; } } async function spliceObject(key) { while (isOperationLocked) { await new Promise(resolve => setTimeout(resolve, 10)); } isOperationLocked = true; try { const index = findIndex(sharedObject, key); sharedObject.splice(index, 1); } finally { isOperationLocked = false; } }
这样两个异步函数不会交叉执行,从根源上避免了时序冲突。
- 使用不可变数据:每次修改
sharedObject时返回新数组,而不是修改原数组。这样readObject拿到的是修改前的数组快照,不会出现访问不存在元素的问题:
let sharedObject = [{key: 'foo', value: {varA: 1, varB: 5}}]; function readObject(key) { const index = findIndex(sharedObject, key); // 用可选链处理可能的undefined const b = sharedObject[index]?.value.varB; } function spliceObject(key) { const index = findIndex(sharedObject, key); // 创建新数组,不修改原数组 sharedObject = sharedObject.filter((_, idx) => idx !== index); }
这种方式更偏向函数式编程风格,也能减少并发修改带来的意外问题。
总结
你的顾虑完全正确,这种异步交叉执行的场景确实会导致错误。核心原因是JavaScript的异步任务调度会让两个函数的执行被穿插,而数组的splice是修改原数组的操作,会影响后续对原数组的访问。通过原子化操作、加锁或者不可变数据的方式,都能有效解决这个问题。
内容的提问来源于stack exchange,提问作者jomak73
相关产品推荐
相关产品推荐

