You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

异步函数共享变量的splice操作:竞态场景下是否触发错误?

异步场景下splice与read操作的冲突问题解答

你担心的这个情况确实会发生,而且这完全是合理的顾虑——这不是你对异步执行机制的误解,而是JavaScript单线程异步模型下很容易出现的并发修改问题。

为什么会出现这个错误?

JavaScript是单线程语言,所有同步代码会一次性执行完毕,不会被打断。但异步任务(比如定时器回调、API响应回调、Promise.then回调等)会被放到任务队列中,等主线程空闲时才会依次执行。

如果readObject和spliceObject是被异步触发的(比如分别在两个setTimeout里,或者两个独立的用户操作回调),它们的执行时机由事件循环调度,完全可能出现这样的时序:

  1. readObject执行到line4,找到了目标元素的index
  2. 主线程切换,执行spliceObject的代码,line10把目标元素从sharedObject中删除
  3. 主线程切回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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 06:37:56