async/await 本身不引发数据竞争,问题在于共享可变状态被多任务并发读写;应使用 actor 隔离状态、避免共享可变引用、取消旧请求确保响应归属、限制并发并串行化关键段。

用 async/await 处理并发请求时,数据竞争不是语法本身的问题,而是共享可变状态被多个异步任务同时读写导致的。关键不在“怎么写 await”,而在“谁在改什么数据、什么时候改”。
用 Actor 隔离共享状态(Swift)
如果你在 Swift 中使用 async/await,Actor 是官方推荐的数据竞争防护机制。它天然隔离内部状态,确保同一时间只有一个任务能访问或修改:
- 把需要保护的数据(比如缓存字典、计数器、用户会话)封装进 actor 内部
- 所有读写操作都通过 actor 的异步方法进行,调用时必须加 await
- 外部无法直接访问 actor 属性,编译器会在编译期检查隔离违规
例如:多个图片加载任务同时尝试更新内存缓存,若缓存字典是全局变量就容易出错;放进 actor 后,每次 cache.set(key, data) 都自动串行化,无需手动加锁。
避免共享可变引用(通用原则)
无论语言,只要不共享可变数据,数据竞争就无从谈起:
- 优先使用不可变结构:请求参数传值不传引用,响应结果用 struct 或 frozen 对象
- 每个请求任务维护自己的局部状态副本,比如单独的 loading 标志、错误对象、临时 buffer
- 不要让多个 async 函数共用同一个 class 实例的可变属性(尤其是非 actor 类型)
常见反例:多个 await 调用共用一个 var result: Data? 属性,且没有同步控制——后完成的请求可能覆盖先完成的结果。
取消旧请求 + 确保结果归属(前端常见场景)
用户快速切换搜索关键词时,前一个请求返回可能污染当前界面。这不是典型的数据竞争,但属于竞态条件,需主动防御:
- 为每次请求生成唯一 token 或 signal,响应回来时校验是否匹配当前有效请求
- 用 cancelable Promise / AbortController(JS)或 Task.cancel()(Swift)中断挂起请求
- 把 resolve/reject 句柄封装进闭包并绑定生命周期,旧请求返回时其句柄已失效
本质是让“谁该处理这个响应”变得明确,而不是靠运气或执行顺序。
限制并发数 + 串行化关键段
当某些操作必须互斥(如写磁盘、更新数据库单条记录),不能只靠 await 自动解决:
- 用信号量(Semaphore)或计数器控制最大并行请求数,避免资源过载
- 对真正需要排他访问的代码块,加一层轻量级队列或锁(如 Swift 的
Task { … }包裹临界区,或 JS 的queueMicrotask协调) - 注意:不要在 await 后面直接操作共享变量,应把整段读-改-写逻辑放在受控上下文中
例如:多个任务都要更新同一个本地统计文件,可以统一交给一个“持久化 manager” actor 处理,其他任务只发消息。











