重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
axiom-swift-concurrency-ref by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swift-concurrency-ref完整的 Swift 并发 API 参考,用于复制粘贴模式和语法查找。
补充 axiom-swift-concurrency(该技能涵盖何时以及为何使用并发——渐进式学习路径、决策树、@concurrent、隔离一致性)。
相关技能:axiom-swift-concurrency(渐进式学习路径、决策树),axiom-synchronization(互斥锁、锁),axiom-assume-isolated(assumeIsolated 模式)
actor ImageCache {
private var cache: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? {
cache[url]
}
func store(_ image: UIImage, for url: URL) {
cache[url] = image
}
}
// 用法 —— 必须跨隔离边界使用 await
let cache = ImageCache()
let image = await cache.image(for: url)
Actor 上的所有属性和方法默认都是隔离的。来自 Actor 隔离域外部的调用者必须使用 await 来访问它们。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
每个 Actor 的存储属性和方法都隔离在该 Actor 内。从隔离边界外部访问需要 await,这会挂起调用者,直到 Actor 能够处理该请求。
actor Counter {
var count = 0 // 隔离的 —— 外部访问需要 await
let name: String // let 常量隐式非隔离
func increment() { // 隔离的 —— 从外部调用需要 await
count += 1
}
nonisolated func identity() -> String {
name // 正确:访问非隔离的 let
}
}
let counter = Counter(name: "main")
await counter.increment() // 必须跨隔离边界使用 await
let id = counter.identity() // 不需要 await —— 非隔离
为同步访问不可变状态选择退出隔离。
actor MyActor {
let id: UUID // let 常量隐式非隔离
nonisolated var description: String {
"Actor \(id)" // 只能访问非隔离状态
}
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(id) // 仅限非隔离属性
}
}
nonisolated 方法不能访问任何隔离的存储属性。将此用于需要同步访问的协议一致性(如 Hashable、CustomStringConvertible)。
Actor 内部的挂起点(await)允许其他调用者交错执行。在任何两个 await 表达式之间,状态都可能发生变化。
actor BankAccount {
var balance: Double = 0
func transfer(amount: Double, to other: BankAccount) async {
guard balance >= amount else { return }
balance -= amount
// 可重入性隐患:在我们等待另一个 Actor 上的存款时,另一个调用者可能在此处修改余额
await other.deposit(amount)
}
func deposit(_ amount: Double) {
balance += amount
}
}
模式:在 Actor 内部的每个 await 之后重新检查状态:
actor BankAccount {
var balance: Double = 0
func transfer(amount: Double, to other: BankAccount) async -> Bool {
guard balance >= amount else { return false }
balance -= amount
await other.deposit(amount)
// 如果需要,在 await 后重新检查不变量
return true
}
}
全局 Actor 提供一个可从任何地方访问的单一共享隔离域。
@globalActor
actor MyGlobalActor {
static let shared = MyGlobalActor()
}
@MyGlobalActor
func doWork() { /* 隔离到 MyGlobalActor */ }
@MyGlobalActor
class MyService {
var state: Int = 0 // 隔离到 MyGlobalActor
}
用于 UI 工作的内置全局 Actor。所有 UI 更新必须在 @MainActor 上发生。
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
func loadItems() async {
let data = await fetchFromNetwork()
items = data // 安全:已在 MainActor 上
}
}
// 注解单个成员
class MixedService {
@MainActor var uiState: String = ""
@MainActor
func updateUI() {
uiState = "Done"
}
func backgroundWork() async -> String {
await heavyComputation()
}
}
子类继承:如果一个类是 @MainActor,所有子类都继承该隔离。
Actor 初始化器不隔离到 Actor。你不能从 init 中调用隔离的方法。
actor DataManager {
var data: [String] = []
init() {
// 不能在此处调用隔离的方法
// self.loadDefaults() // 错误:在非隔离的 init 中调用 actor 隔离的方法
}
// 改用工厂方法
static func create() async -> DataManager {
let manager = DataManager()
await manager.loadDefaults()
return manager
}
func loadDefaults() {
data = ["default"]
}
}
| 注意事项 | 症状 | 修复方法 |
|---|---|---|
| Actor 可重入性 | 在 await 之间状态改变 | 在每个 await 后重新检查状态 |
| nonisolated 访问隔离状态 | 编译器错误 | 移除 nonisolated 或使属性非隔离 |
| 从同步上下文调用 actor 方法 | "表达式是 'async'" | 用 Task {} 包装或使调用者异步 |
| 全局 Actor 继承 | 子类继承 @MainActor | 有意识地决定哪些方法需要隔离 |
| Actor init 不隔离 | 不能在 init 中调用隔离的方法 | 使用工厂方法或在 init 后填充 |
| Actor 协议一致性 | "非隔离"一致性错误 | 对协议方法使用 nonisolated,或使用隔离一致性(Swift 6.2+) |
当所有存储属性都是 Sendable 时,值类型是 Sendable。
// 结构体:当所有存储属性都是 Sendable 时,是 Sendable
struct UserProfile: Sendable {
let name: String
let age: Int
}
// 枚举:当所有关联值都是 Sendable 时,是 Sendable
enum LoadState: Sendable {
case idle
case loading
case loaded(String) // String 是 Sendable
case failed(Error) // 错误:Error 不是 Sendable
}
// 修复:使用 Sendable 错误类型
enum LoadState: Sendable {
case idle
case loading
case loaded(String)
case failed(any Error & Sendable)
}
跨隔离边界传递的闭包必须是 @Sendable。@Sendable 闭包不能捕获可变的局部状态。
func runInBackground(_ work: @Sendable () -> Void) {
Task.detached { work() }
}
// 所有捕获的值必须是 Sendable
var count = 0
runInBackground {
// 错误:捕获可变局部变量
// count += 1
}
let snapshot = count
runInBackground {
print(snapshot) // 正确:Sendable 类型的 let 绑定
}
手动保证线程安全。仅在你自行提供同步时使用。
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Any] = [:]
func get(_ key: String) -> Any? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
func set(_ key: String, value: Any) {
lock.lock()
defer { lock.unlock() }
storage[key] = value
}
}
@unchecked Sendable 的要求:
finalstruct Box<T> {
let value: T
}
// 仅当 T 是 Sendable 时,Box 才是 Sendable
extension Box: Sendable where T: Sendable {}
// 标准库广泛使用此模式:
// Array<Element>: Sendable where Element: Sendable
// Dictionary<Key, Value>: Sendable where Key: Sendable, Value: Sendable
// Optional<Wrapped>: Sendable where Wrapped: Sendable
跨隔离边界转移值的所有权。调用者放弃访问权。
func process(_ value: sending String) async {
// 调用者在此次调用后无法再访问 value
await store(value)
}
// 当调用者不再使用非 Sendable 类型时,用于转移它们
func handOff(_ connection: sending NetworkConnection) async {
await manager.accept(connection)
}
控制 Xcode 中 Sendable 检查的严格程度:
| 设置 | 值 | 行为 |
|---|---|---|
SWIFT_STRICT_CONCURRENCY | minimal | 仅检查显式的 Sendable 注解 |
SWIFT_STRICT_CONCURRENCY | targeted | 推断的 Sendable + 闭包检查 |
SWIFT_STRICT_CONCURRENCY | complete | 完全严格并发(Swift 6 默认) |
| 注意事项 | 症状 | 修复方法 |
|---|---|---|
| 类不能是 Sendable | "类无法符合 Sendable" | 设为 final + 不可变,或使用带锁的 @unchecked Sendable |
| 闭包捕获非 Sendable | "捕获非 Sendable 类型" | 在捕获前复制值,或使类型成为 Sendable |
| 协议不能要求 Sendable | 泛型约束复杂 | 使用 where T: Sendable |
| @unchecked Sendable 隐藏错误 | 运行时数据竞争 | 仅在锁/队列保证安全时使用 |
| Array/Dictionary 条件性 | 仅当元素是 Sendable 时集合才是 Sendable | 确保元素类型是 Sendable |
| Error 不是 Sendable | "类型不符合 Sendable" | 使用 any Error & Sendable 或类型化错误 |
创建一个继承当前 Actor 上下文和优先级的非结构化任务。
// 继承 Actor 上下文 —— 如果从 @MainActor 调用,则在 MainActor 上运行
let task = Task {
try await fetchData()
}
// 获取结果
let result = try await task.value
// 获取 Result<Success, Failure>
let outcome = await task.result
创建一个没有继承上下文的任务。不继承 Actor 或优先级。
Task.detached(priority: .background) {
// 即使从 MainActor 创建,也不在 MainActor 上运行
await processLargeFile()
}
何时使用:必须在不在调用 Actor 上运行的后台工作。在大多数情况下优先使用 Task {} —— 很少需要 Task.detached。
取消是协作式的。设置取消是一个请求;任务必须检查并响应。
let task = Task {
for item in largeCollection {
// 选项 1:检查布尔值
if Task.isCancelled { break }
// 选项 2:抛出 CancellationError
try Task.checkCancellation()
await process(item)
}
}
// 请求取消
task.cancel()
挂起当前任务一段时间。支持取消——如果在睡眠期间被取消,则抛出 CancellationError。
// 基于时长(首选)
try await Task.sleep(for: .seconds(2))
try await Task.sleep(for: .milliseconds(500))
// 纳秒(旧 API)
try await Task.sleep(nanoseconds: 2_000_000_000)
自愿让出执行权,以允许其他任务运行。在长时间运行的同步循环中使用。
for i in 0..<1_000_000 {
if i.isMultiple(of: 1000) {
await Task.yield()
}
process(i)
}
| 优先级 | 使用场景 |
|---|---|
.userInitiated | 直接用户操作,可见结果 |
.high | 与 .userInitiated 相同 |
.medium | 未指定时的默认值 |
.low | 预取,非紧急工作 |
.utility | 长时间计算,显示进度 |
.background | 维护、清理,时间不敏感 |
Task(priority: .userInitiated) {
await loadVisibleContent()
}
Task(priority: .background) {
await cleanupTempFiles()
}
任务作用域的值,自动传播到子任务。
enum RequestContext {
@TaskLocal static var requestID: String?
@TaskLocal static var userID: String?
}
// 为作用域设置值
RequestContext.$requestID.withValue("req-123") {
RequestContext.$userID.withValue("user-456") {
// 在此处和子任务中两个值都可用
Task {
print(RequestContext.requestID) // "req-123"
print(RequestContext.userID) // "user-456"
}
}
}
// 作用域外 —— 值为 nil
print(RequestContext.requestID) // nil
传播规则:@TaskLocal 值传播到使用 Task {} 创建的子任务。它们不传播到 Task.detached {}。
| 注意事项 | 症状 | 修复方法 |
|---|---|---|
| 任务从未被取消 | 资源泄漏,视图消失后工作继续 | 存储任务,在 deinit/onDisappear 中取消 |
| 忽略取消 | 即使被取消,任务也运行完成 | 在循环中检查 Task.isCancelled,使用 checkCancellation() |
| Task.detached 丢失 Actor 上下文 | "未隔离到 MainActor" | 当你需要 Actor 隔离时使用 Task {} |
| 在 Task 中捕获 self | 潜在的循环引用 | 对长期存在的任务使用 [weak self] |
| TaskLocal 未传播 | 在 detached 任务中值为 nil | TaskLocal 仅传播到子任务,不传播到 detached 任务 |
| 任务优先级反转 | 低优先级任务阻塞高优先级任务 | 系统处理大多数情况;避免从高优先级任务中 await 低优先级任务 |
并行运行固定数量的操作。所有 async let 绑定在作用域退出时隐式地等待。
async let images = fetchImages()
async let metadata = fetchMetadata()
async let config = loadConfig()
// 三个操作并发运行,一起等待
let (imgs, meta, cfg) = try await (images, metadata, config)
语义:如果一个 async let 抛出错误,其他会被取消。所有操作必须完成(或被取消),然后封闭作用域才能退出。
动态数量的并行任务,且都不抛出错误。
let results = await withTaskGroup(of: String.self) { group in
for name in names {
group.addTask {
await fetchGreeting(for: name)
}
}
var greetings: [String] = []
for await greeting in group {
greetings.append(greeting)
}
return greetings
}
动态数量的并行任务,可能抛出错误。
let images = try await withThrowingTaskGroup(of: (URL, UIImage).self) { group in
for url in urls {
group.addTask {
let image = try await downloadImage(url)
return (url, image)
}
}
var results: [URL: UIImage] = [:]
for try await (url, image) in group {
results[url] = image
}
return results
}
用于需要并发但不需要收集结果的情况。
try await withThrowingDiscardingTaskGroup { group in
for connection in connections {
group.addTask {
try await connection.monitor()
// 结果被丢弃 —— 适用于长时间运行的服务
}
}
// 组保持活动直到所有任务完成或一个抛出错误
}
await withTaskGroup(of: Data.self) { group in
// 有条件地添加任务
group.addTaskUnlessCancelled {
await fetchData()
}
// 取消剩余任务
group.cancelAll()
// 等待而不收集
await group.waitForAll()
// 一次迭代一个
while let result = await group.next() {
process(result)
}
}
结构化并发形成一个树:
父级取消会取消所有子级 —— 取消一个任务会取消所有 async let 和 TaskGroup 子级
子级错误传播到父级 —— 在抛出错误的组中,一个子级错误会取消兄弟任务并向上传播
所有子级必须在父级返回前完成 —— 作用域等待所有子级,即使是被取消的
// 如果 fetchImages() 抛出错误,fetchMetadata() 会自动被取消 async let images = fetchImages() async let metadata = fetchMetadata() let result = try await (images, metadata)
| 注意事项 | 症状 | 修复方法 |
|---|---|---|
| async let 未使用 | 工作仍会执行但结果被静默丢弃 | 分配所有 async let 结果或使用 withDiscardingTaskGroup |
| TaskGroup 累积内存 | 内存随 10K+ 任务增长 | 在结果到达时处理,不要收集所有 |
| 在 addTask 中捕获可变状态 | "捕获的 var 发生突变" | 使用 let 绑定或 actor |
| 未处理部分失败 | 一些任务成功,一些失败 | 使用 group.next() 并单独处理错误 |
| 循环中的 async let | 编译器错误 —— async let 必须在固定位置 | 改用 TaskGroup |
| 从组中提前返回 | 剩余任务仍在运行 | 在返回前调用 group.cancelAll() |
用于随时间产生值的非抛出流。
let stream = AsyncStream<Int> { continuation in
for i in 0..<10 {
continuation.yield(i)
}
continuation.finish()
}
for await value in stream {
print(value)
}
可能因错误而失败的流。
let stream = AsyncThrowingStream<Data, Error> { continuation in
let monitor = NetworkMonitor()
monitor.onData = { data in
continuation.yield(data)
}
monitor.onError = { error in
continuation.finish(throwing: error)
}
monitor.onComplete = {
continuation.finish()
}
continuation.onTermination = { @Sendable _ in
monitor.stop()
}
monitor.start()
}
do {
for try await data in stream {
process(data)
}
} catch {
handleStreamError(error)
}
let stream = AsyncStream<Value> { continuation in
// 发出一个值
continuation.yield(value)
// 正常结束流
continuation.finish()
// 当消费者取消或流结束时进行清理
continuation.onTermination = { @Sendable termination in
switch termination {
case .cancelled:
cleanup()
case .finished:
finalCleanup()
@unknown default:
break
}
}
}
// 对于抛出错误的流
let stream = AsyncThrowingStream<Value, Error> { continuation in
continuation.yield(value)
continuation.finish() // 正常结束
continuation.finish(throwing: error) // 以错误结束
}
控制当值产生速度快于消费速度时发生的情况。
// 保留所有值(默认)—— 内存可能无限增长
let stream = AsyncStream<Int>(bufferingPolicy: .unbounded) { continuation in
// ...
}
// 保留最旧的 N 个值,当缓冲区满时丢弃新的
let stream = AsyncStream<Int>(bufferingPolicy: .bufferingOldest(100)) { continuation in
// ...
}
// 保留最新的 N 个值,当缓冲区满时丢弃旧的
let stream = AsyncStream<Int>(bufferingPolicy: .bufferingNewest(100)) { continuation in
// ...
}
| 策略 | 行为 | 使用场景 |
|---|---|---|
.unbounded | 保留所有值 | 消费者能跟上,或生产者有界 |
.bufferingOldest(N) | 缓冲区满时丢弃新值 | 顺序重要,旧值优先 |
.bufferingNewest(N) | 缓冲区满时丢弃旧值 | 最新状态重要(UI 更新、传感器数据) |
struct Counter: AsyncSequence {
typealias Element = Int
let limit: Int
struct AsyncIterator: AsyncIteratorProtocol {
var current = 0
let limit: Int
mutating func next() async -> Int? {
guard current < limit else { return nil }
defer { current += 1 }
return current
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(limit: limit)
}
}
// 用法
for await number in Counter(limit: 5) {
print(number) // 0, 1, 2, 3, 4
}
标准操作符适用于任何 AsyncSequence:
// Map
for await name in users.map(\.name) { }
// Filter
for await adult in users.filter({ $0.age >= 18 }) { }
// CompactMap
for await image in urls.compactMap({ await tryLoadImage($0) }) { }
// Prefix
for await first5 in stream.prefix(5) { }
// first(where:)
let match = await stream.first(where: { $0 > threshold })
// Contains
let hasMatch = await stream.contains(where: { $0 > threshold })
// Reduce
let sum = await numbers.reduce(0, +)
// NotificationCenter
for await notification in NotificationCenter.default.notifications(named: .didUpdate) {
handleUpdate(notification)
}
// URLSession bytes
let (bytes, response) = try await URLSession.shared.bytes(from: url)
for try await byte in bytes {
process(byte)
}
// FileHandle bytes
for try await line in FileHandle.standardInput.bytes.lines {
process(line)
}
| 注意事项 | 症状 | 修复方法 |
|---|---|---|
| Continuation 在 finish 后 yield | 运行时警告,值丢失 | 跟踪完成状态,在 yield 前检查 |
| 流永不结束 | for-await 循环永远挂起 | 在所有代码路径中始终调用 continuation.finish() |
| 没有 onTermination 处理程序 | 消费者取消时资源泄漏 | 设置 continuation.onTermination 进行清理 |
| 无界缓冲区 | 负载下内存增长 | 使用 .bufferingNewest(N) 或 .bufferingOldest(N) |
| 多个消费者 | 只有第一个消费者获得值 | AsyncStream 是单消费者的;为每个消费者创建单独的流 |
| 在 MainActor 上使用 for-await | UI 冻结等待值 | 使用 Task {} 在非主路径上消费 |
@MainActor
func updateUI() {
label.text = "Done"
}
// 从异步上下文调用
func doWork() async {
let result = await computeResult()
await updateUI() // 跳转到 MainActor
}
从任何上下文显式地在主 Actor 上执行一个闭包。
func processData() async {
let result = await heavyComputation()
await MainActor.run {
self.label.text = result
self.progressView.isHidden = true
}
}
断言代码已经在主 Actor 上运行。如果断言为假,则在运行时崩溃。
func legacyCallback() {
// 我们知道这是在主线程上调用的(UIKit 保证)
MainActor.assumeIsolated {
self.viewModel.update() // 访问 @MainActor 状态
}
}
有关全面模式,请参阅 axiom-assume-isolated。
选择退出封闭 Actor 的隔离。
@MainActor
class ViewModel {
let id: UUID // 隐式非隔离(let)
nonisolated var analyticsID: String { // 显式非隔离
id.uuidString
}
var items: [Item] = [] // 隔离到 MainActor
}
编译器逃生舱。告诉编译器将属性视为未隔离,不提供任何安全保证。
// 仅当你有外部线程安全保证时使用
nonisolated(unsafe) var legacyState: Int = 0
// 常用于编译器无法验证的全局常量
nonisolated(unsafe) let formatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
return f
}()
警告:nonisolated(unsafe) 不提供任何运行时保护。数据竞争不会被捕获。仅作为桥接遗留代码的最后手段使用。
在迁移期间抑制对前并发 API 的并发警告。
// 抑制整个模块的警告
@preconcurrency import MyLegacyFramework
// 抑制特定协议一致性
class MyDelegate: @preconcurrency SomeLegacyDelegate {
func delegateCallback() {
// 此一致性没有 Sendable 警告
}
}
捕获调用者的隔离上下文,使函数在调用者所在的任何 Actor 上运行。
func doWork(isolation: isolated (any Actor)? = #isolation) async {
// 在调用者的 Actor 上运行 —— 如果调用者已经隔离,则无需跳转
performWork()
}
// 从 @MainActor 调用 —— 在 MainActor 上运行
@MainActor
func setup() async {
await doWork() // doWork 在 MainActor 上运行
}
// 从自定义 actor 调用 —— 在该 actor 上运行
actor MyActor {
func run() async {
await doWork() // doWork 在 MyActor 上运行
}
}
| 注意事项 | 症状 | 修复方法 |
|---|---|---|
| 从 MainActor 调用 MainActor.run | 不必要的跳转,潜在死锁风险 | 检查上下文或使用 assumeIsolated |
| nonisolated(unsafe) 数据竞争 | 运行时崩溃,状态损坏 | 使用适当的隔离或互斥锁 |
| @preconcurrency 隐藏真正问题 | 生产环境运行时崩溃 | 在发布前迁移到适当的并发 |
| #isolation 在 5.9 之前不可用 | 编译器错误 | 使用传统的 @MainActor 注解 |
| actor 方法上的 nonisolated | 无法访问任何隔离状态 | 仅用于从非隔离状态计算的属性 |
将基于回调的 API 桥接到 async/await。
非抛出桥接。
func currentLocation() async -> CLLocation {
await withCheckedContinuation { continuation in
locationManager.requestLocation { location in
continuation.resume(returning: location)
}
}
}
抛出桥接。
func fetchUser(id: String) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
api.fetchUser(id: id) { result in
switch result {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
// 返回一个值
continuation.resume(returning: value)
// 抛出一个错误
continuation.resume(throwing: error)
// 从 Result 类型
continuation.resume(with: result) // Result<T, Error>
一个 Continuation 必须恰好恢复一次:
恢复两次会因 "Continuation already resumed"(已检查)或未定义行为(不安全)而崩溃
从不恢复会导致等待的任务永远挂起——静默泄漏
// 危险:回调可能不会被调用 func riskyBridge() async throws -> Data { try await withCheckedThrowingContinuation { continuation in api.fetch { data, error in if let error { continuation.resume(throwing: error) return } if let data { continuation.resume(returning: data) return } // 错误:如果两者都为 nil,continuation 永远不会恢复 // 修复:添加后备方案 continuation.resume(throwing: BridgeError.noResponse) } } }
class LocationBridge: NSObject, CLLocationManagerDelegate {
private var continuation: CheckedContinuation<CLLocation, Error>?
private let manager = CLLocationManager()
func requestLocation() async throws -> CLLocation {
try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
manager.delegate = self
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
continuation?.resume(returning: locations[0])
continuation = nil // 防止双重恢复
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
continuation?.resume(throwing: error)
continuation = nil
}
}
跳过运行时检查以提高性能。API 与已检查的相同,但误用会导致未定义行为,而不是诊断性崩溃。
func fastBridge() async -> Data {
await withUnsafeContinuation { continuation in
// 没有针对双重恢复或缺失恢复的运行时检查
fastCallback { data in
continuation.resume(returning: data)
}
}
}
在开发期间使用已检查的 continuations,只有在经过彻底测试且性能分析显示检查是瓶颈时才切换到不安全的。
| 注意事项 | 症状 | 修复方法 |
|---|---|---|
| 恢复被调用两次 | "Continuation already resumed" 崩溃 | 恢复后将 continuation 设为 nil |
| 恢复从未被调用 | 任务无限期挂起 | 确保所有代码路径都恢复——包括错误/nil 情况 |
| 捕获 continuation | Continuation 逃逸作用域 | 存储在属性中,确保单次恢复 |
| 调试中的不安全 continuation | 对误用没有诊断 | 在开发期间使用 withCheckedContinuation |
| 委托被多次调用 | 第二次恢复时崩溃 | 对于重复回调,使用 AsyncStream 而不是 continuation |
| 回调在错误的线程上 | 对 continuation 无关紧要 | Continuations 可以从任何线程恢复 |
从 GCD 和完成处理程序迁移到 Swift 并发的常见模式。
// 之前:使用 DispatchQueue 实现线程安全
class ImageCache {
private let queue = DispatchQueue(label: "cache", attributes: .concurrent)
private var cache: [URL: UIImage] = [:]
func get(_ url: URL, completion: @escaping (UIImage?) -> Void) {
queue.async { completion(self.cache[url]) }
}
func set(_ url: URL, image: UIImage) {
queue.async(flags: .barrier) { self.cache[url] = image }
}
}
// 之后:Actor
actor ImageCache {
private var cache: [URL: UIImage] = [:]
func get(_ url: URL) -> UIImage? {
cache[url]
}
func set(_ url: URL, image: UIImage) {
cache[url] = image
}
}
// 之前:DispatchGroup
let group = DispatchGroup()
var results: [Data] = []
for url in urls {
group.enter()
fetch(url) { data in
results.append(data)
group.leave()
}
}
group.notify(queue: .main) { use(results) }
// 之后:TaskGroup
let results = await withTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask { await fetch(url) }
}
var collected: [Data] = []
for await data in group {
collected.append(data)
}
return collected
}
use(results)
// 之前
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error { completion(.failure(error)); return }
guard let data else { completion(.failure(FetchError.noData)); return }
completion(.success(data))
}.resume()
}
// 之后
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
@MainActor
class ViewController: UIViewController, UITableViewDelegate {
// @objc 委托方法从类继承 @MainActor 隔离
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 已经在 MainActor 上 —— 安全更新 UI
updateSelection(indexPath)
}
}
// 之前
let observer = NotificationCenter.default.addObserver(
forName: .didUpdate, object: nil, queue: .main
) { notification in
handleUpdate(notification)
}
// 必须在 deinit 中移除观察者
// 之后
let task = Task {
for await notification in NotificationCenter.default.notifications(named: .didUpdate) {
await handleUpdate(notification)
}
}
// 在 deinit 中取消任务 —— 无需手动移除观察者
// 之前
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
updateUI()
}
// 必须在 deinit 中使定时器失效
// 之后
let task = Task {
while !Task.isCancelled {
await updateUI()
try? await Task.sleep(for: .seconds(1))
}
}
// 在 deinit 中取消任务
// 之前:使用信号量限制并发操作
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
Complete Swift concurrency API reference for copy-paste patterns and syntax lookup.
Complements axiom-swift-concurrency (which covers when and why to use concurrency — progressive journey, decision trees, @concurrent, isolated conformances).
Related skills : axiom-swift-concurrency (progressive journey, decision trees), axiom-synchronization (Mutex, locks), axiom-assume-isolated (assumeIsolated patterns)
actor ImageCache {
private var cache: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? {
cache[url]
}
func store(_ image: UIImage, for url: URL) {
cache[url] = image
}
}
// Usage — must await across isolation boundary
let cache = ImageCache()
let image = await cache.image(for: url)
All properties and methods on an actor are isolated by default. Callers outside the actor's isolation domain must use await to access them.
Every actor's stored properties and methods are isolated to that actor. Access from outside the isolation boundary requires await, which suspends the caller until the actor can process the request.
actor Counter {
var count = 0 // Isolated — external access requires await
let name: String // let constants are implicitly nonisolated
func increment() { // Isolated — await required from outside
count += 1
}
nonisolated func identity() -> String {
name // OK: accessing nonisolated let
}
}
let counter = Counter(name: "main")
await counter.increment() // Must await across isolation boundary
let id = counter.identity() // No await needed — nonisolated
Opt out of isolation for synchronous access to non-mutable state.
actor MyActor {
let id: UUID // let constants are implicitly nonisolated
nonisolated var description: String {
"Actor \(id)" // Can only access nonisolated state
}
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(id) // Only nonisolated properties
}
}
nonisolated methods cannot access any isolated stored properties. Use this for protocol conformances (like Hashable, CustomStringConvertible) that require synchronous access.
Suspension points (await) inside an actor allow other callers to interleave. State may change between any two await expressions.
actor BankAccount {
var balance: Double = 0
func transfer(amount: Double, to other: BankAccount) async {
guard balance >= amount else { return }
balance -= amount
// REENTRANCY HAZARD: another caller could modify balance here
// while we await the deposit on the other actor
await other.deposit(amount)
}
func deposit(_ amount: Double) {
balance += amount
}
}
Pattern : Re-check state after every await inside an actor:
actor BankAccount {
var balance: Double = 0
func transfer(amount: Double, to other: BankAccount) async -> Bool {
guard balance >= amount else { return false }
balance -= amount
await other.deposit(amount)
// Re-check invariants after await if needed
return true
}
}
A global actor provides a single shared isolation domain accessible from anywhere.
@globalActor
actor MyGlobalActor {
static let shared = MyGlobalActor()
}
@MyGlobalActor
func doWork() { /* isolated to MyGlobalActor */ }
@MyGlobalActor
class MyService {
var state: Int = 0 // Isolated to MyGlobalActor
}
The built-in global actor for UI work. All UI updates must happen on @MainActor.
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
func loadItems() async {
let data = await fetchFromNetwork()
items = data // Safe: already on MainActor
}
}
// Annotate individual members
class MixedService {
@MainActor var uiState: String = ""
@MainActor
func updateUI() {
uiState = "Done"
}
func backgroundWork() async -> String {
await heavyComputation()
}
}
Subclass inheritance : If a class is @MainActor, all subclasses inherit that isolation.
Actor initializers are NOT isolated to the actor. You cannot call isolated methods from init.
actor DataManager {
var data: [String] = []
init() {
// Cannot call isolated methods here
// self.loadDefaults() // ERROR: actor-isolated method in non-isolated init
}
// Use a factory method instead
static func create() async -> DataManager {
let manager = DataManager()
await manager.loadDefaults()
return manager
}
func loadDefaults() {
data = ["default"]
}
}
| Gotcha | Symptom | Fix |
|---|---|---|
| Actor reentrancy | State changes between awaits | Re-check state after each await |
| nonisolated accessing isolated state | Compiler error | Remove nonisolated or make property nonisolated |
| Calling actor method from sync context | "Expression is 'async'" | Wrap in Task {} or make caller async |
| Global actor inheritance | Subclass inherits @MainActor | Be intentional about which methods need isolation |
| Actor init not isolated | Can't call isolated methods in init | Use factory method or populate after init |
| Actor protocol conformance | "Non-isolated" conformance error | Use nonisolated for protocol methods, or isolated conformance (Swift 6.2+) |
Value types are Sendable when all stored properties are Sendable.
// Structs: Sendable when all stored properties are Sendable
struct UserProfile: Sendable {
let name: String
let age: Int
}
// Enums: Sendable when all associated values are Sendable
enum LoadState: Sendable {
case idle
case loading
case loaded(String) // String is Sendable
case failed(Error) // ERROR: Error is not Sendable
}
// Fix: use a Sendable error type
enum LoadState: Sendable {
case idle
case loading
case loaded(String)
case failed(any Error & Sendable)
}
Closures passed across isolation boundaries must be @Sendable. A @Sendable closure cannot capture mutable local state.
func runInBackground(_ work: @Sendable () -> Void) {
Task.detached { work() }
}
// All captured values must be Sendable
var count = 0
runInBackground {
// ERROR: capture of mutable local variable
// count += 1
}
let snapshot = count
runInBackground {
print(snapshot) // OK: let binding of Sendable type
}
Manual guarantee of thread safety. Use only when you provide synchronization yourself.
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Any] = [:]
func get(_ key: String) -> Any? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
func set(_ key: String, value: Any) {
lock.lock()
defer { lock.unlock() }
storage[key] = value
}
}
Requirements for @unchecked Sendable :
finalstruct Box<T> {
let value: T
}
// Box is Sendable only when T is Sendable
extension Box: Sendable where T: Sendable {}
// Standard library uses this extensively:
// Array<Element>: Sendable where Element: Sendable
// Dictionary<Key, Value>: Sendable where Key: Sendable, Value: Sendable
// Optional<Wrapped>: Sendable where Wrapped: Sendable
Transfer ownership of a value across isolation boundaries. The caller gives up access.
func process(_ value: sending String) async {
// Caller can no longer access value after this call
await store(value)
}
// Useful for transferring non-Sendable types when caller won't use them again
func handOff(_ connection: sending NetworkConnection) async {
await manager.accept(connection)
}
Control the strictness of Sendable checking in Xcode:
| Setting | Value | Behavior |
|---|---|---|
SWIFT_STRICT_CONCURRENCY | minimal | Only explicit Sendable annotations checked |
SWIFT_STRICT_CONCURRENCY | targeted | Inferred Sendable + closure checking |
SWIFT_STRICT_CONCURRENCY | complete | Full strict concurrency (Swift 6 default) |
| Gotcha | Symptom | Fix |
|---|---|---|
| Class can't be Sendable | "Class cannot conform to Sendable" | Make final + immutable, or @unchecked Sendable with locks |
| Closure captures non-Sendable | "Capture of non-Sendable type" | Copy value before capture, or make type Sendable |
| Protocol can't require Sendable | Generic constraints complex | Use where T: Sendable |
| @unchecked Sendable hides bugs | Data races at runtime | Only use when lock/queue guarantees safety |
| Array/Dictionary conditional | Collection is Sendable only if Element is | Ensure element types are Sendable |
| Error not Sendable | "Type does not conform to Sendable" | Use any Error & Sendable or typed errors |
Creates an unstructured task that inherits the current actor context and priority.
// Inherits actor context — if called from @MainActor, runs on MainActor
let task = Task {
try await fetchData()
}
// Get the result
let result = try await task.value
// Get Result<Success, Failure>
let outcome = await task.result
Creates a task with no inherited context. Does not inherit the actor or priority.
Task.detached(priority: .background) {
// NOT on MainActor even if created from MainActor
await processLargeFile()
}
When to use : Background work that must NOT run on the calling actor. Prefer Task {} in most cases — Task.detached is rarely needed.
Cancellation is cooperative. Setting cancellation is a request; the task must check and respond.
let task = Task {
for item in largeCollection {
// Option 1: Check boolean
if Task.isCancelled { break }
// Option 2: Throw CancellationError
try Task.checkCancellation()
await process(item)
}
}
// Request cancellation
task.cancel()
Suspends the current task for a duration. Supports cancellation — throws CancellationError if cancelled during sleep.
// Duration-based (preferred)
try await Task.sleep(for: .seconds(2))
try await Task.sleep(for: .milliseconds(500))
// Nanoseconds (older API)
try await Task.sleep(nanoseconds: 2_000_000_000)
Voluntarily yields execution to allow other tasks to run. Use in long-running synchronous loops.
for i in 0..<1_000_000 {
if i.isMultiple(of: 1000) {
await Task.yield()
}
process(i)
}
| Priority | Use Case |
|---|---|
.userInitiated | Direct user action, visible result |
.high | Same as .userInitiated |
.medium | Default when not specified |
.low | Prefetching, non-urgent work |
.utility | Long computation, progress shown |
.background | Maintenance, cleanup, not time-sensitive |
Task(priority: .userInitiated) {
await loadVisibleContent()
}
Task(priority: .background) {
await cleanupTempFiles()
}
Task-scoped values that propagate to child tasks automatically.
enum RequestContext {
@TaskLocal static var requestID: String?
@TaskLocal static var userID: String?
}
// Set values for a scope
RequestContext.$requestID.withValue("req-123") {
RequestContext.$userID.withValue("user-456") {
// Both values available here and in child tasks
Task {
print(RequestContext.requestID) // "req-123"
print(RequestContext.userID) // "user-456"
}
}
}
// Outside scope — values are nil
print(RequestContext.requestID) // nil
Propagation rules : @TaskLocal values propagate to child tasks created with Task {}. They do NOT propagate to Task.detached {}.
| Gotcha | Symptom | Fix |
|---|---|---|
| Task never cancelled | Resource leak, work continues after view disappears | Store task, cancel in deinit/onDisappear |
| Ignoring cancellation | Task runs to completion even when cancelled | Check Task.isCancelled in loops, use checkCancellation() |
| Task.detached loses actor context | "Not isolated to MainActor" | Use Task {} when you need actor isolation |
| Capturing self in Task | Potential retain cycle | Use [weak self] for long-lived tasks |
| TaskLocal not propagated | Value is nil in detached task | TaskLocal only propagates to child tasks, not detached |
| Task priority inversion | Low-priority task blocks high-priority | System handles most cases; avoid awaiting low-priority from high |
Run a fixed number of operations in parallel. All async let bindings are implicitly awaited when the scope exits.
async let images = fetchImages()
async let metadata = fetchMetadata()
async let config = loadConfig()
// All three run concurrently, await together
let (imgs, meta, cfg) = try await (images, metadata, config)
Semantics : If one async let throws, the others are cancelled. All must complete (or be cancelled) before the enclosing scope exits.
Dynamic number of parallel tasks where none throw.
let results = await withTaskGroup(of: String.self) { group in
for name in names {
group.addTask {
await fetchGreeting(for: name)
}
}
var greetings: [String] = []
for await greeting in group {
greetings.append(greeting)
}
return greetings
}
Dynamic number of parallel tasks that can throw.
let images = try await withThrowingTaskGroup(of: (URL, UIImage).self) { group in
for url in urls {
group.addTask {
let image = try await downloadImage(url)
return (url, image)
}
}
var results: [URL: UIImage] = [:]
for try await (url, image) in group {
results[url] = image
}
return results
}
For when you need concurrency but don't need to collect results.
try await withThrowingDiscardingTaskGroup { group in
for connection in connections {
group.addTask {
try await connection.monitor()
// Results are discarded — useful for long-running services
}
}
// Group stays alive until all tasks complete or one throws
}
await withTaskGroup(of: Data.self) { group in
// Add tasks conditionally
group.addTaskUnlessCancelled {
await fetchData()
}
// Cancel remaining tasks
group.cancelAll()
// Wait without collecting
await group.waitForAll()
// Iterate one at a time
while let result = await group.next() {
process(result)
}
}
Structured concurrency forms a tree:
Parent cancellation cancels all children — cancelling a task cancels all async let and TaskGroup children
Child error propagates to parent — in throwing groups, a child error cancels siblings and propagates up
All children must complete before parent returns — the scope awaits all children, even cancelled ones
// If fetchImages() throws, fetchMetadata() is automatically cancelled async let images = fetchImages() async let metadata = fetchMetadata() let result = try await (images, metadata)
| Gotcha | Symptom | Fix |
|---|---|---|
| async let unused | Work still executes but result is discarded silently | Assign all async let results or use withDiscardingTaskGroup |
| TaskGroup accumulating memory | Memory grows with 10K+ tasks | Process results as they arrive, don't collect all |
| Capturing mutable state in addTask | "Mutation of captured var" | Use let binding or actor |
| Not handling partial failure | Some tasks succeed, some fail | Use group.next() and handle errors individually |
| async let in loop | Compiler error — async let must be in fixed positions | Use TaskGroup instead |
| Returning from group early | Remaining tasks still run | Call group.cancelAll() before returning |
Non-throwing stream for producing values over time.
let stream = AsyncStream<Int> { continuation in
for i in 0..<10 {
continuation.yield(i)
}
continuation.finish()
}
for await value in stream {
print(value)
}
Stream that can fail with an error.
let stream = AsyncThrowingStream<Data, Error> { continuation in
let monitor = NetworkMonitor()
monitor.onData = { data in
continuation.yield(data)
}
monitor.onError = { error in
continuation.finish(throwing: error)
}
monitor.onComplete = {
continuation.finish()
}
continuation.onTermination = { @Sendable _ in
monitor.stop()
}
monitor.start()
}
do {
for try await data in stream {
process(data)
}
} catch {
handleStreamError(error)
}
let stream = AsyncStream<Value> { continuation in
// Emit a value
continuation.yield(value)
// End the stream normally
continuation.finish()
// Cleanup when consumer cancels or stream ends
continuation.onTermination = { @Sendable termination in
switch termination {
case .cancelled:
cleanup()
case .finished:
finalCleanup()
@unknown default:
break
}
}
}
// For throwing streams
let stream = AsyncThrowingStream<Value, Error> { continuation in
continuation.yield(value)
continuation.finish() // Normal end
continuation.finish(throwing: error) // End with error
}
Control what happens when values are produced faster than consumed.
// Keep all values (default) — memory can grow unbounded
let stream = AsyncStream<Int>(bufferingPolicy: .unbounded) { continuation in
// ...
}
// Keep oldest N values, drop new ones when buffer is full
let stream = AsyncStream<Int>(bufferingPolicy: .bufferingOldest(100)) { continuation in
// ...
}
// Keep newest N values, drop old ones when buffer is full
let stream = AsyncStream<Int>(bufferingPolicy: .bufferingNewest(100)) { continuation in
// ...
}
| Policy | Behavior | Use When |
|---|---|---|
.unbounded | Keeps all values | Consumer keeps up, or bounded producer |
.bufferingOldest(N) | Drops new values when full | Order matters, older values have priority |
.bufferingNewest(N) | Drops old values when full | Latest state matters (UI updates, sensor data) |
struct Counter: AsyncSequence {
typealias Element = Int
let limit: Int
struct AsyncIterator: AsyncIteratorProtocol {
var current = 0
let limit: Int
mutating func next() async -> Int? {
guard current < limit else { return nil }
defer { current += 1 }
return current
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(limit: limit)
}
}
// Usage
for await number in Counter(limit: 5) {
print(number) // 0, 1, 2, 3, 4
}
Standard operators work on any AsyncSequence:
// Map
for await name in users.map(\.name) { }
// Filter
for await adult in users.filter({ $0.age >= 18 }) { }
// CompactMap
for await image in urls.compactMap({ await tryLoadImage($0) }) { }
// Prefix
for await first5 in stream.prefix(5) { }
// first(where:)
let match = await stream.first(where: { $0 > threshold })
// Contains
let hasMatch = await stream.contains(where: { $0 > threshold })
// Reduce
let sum = await numbers.reduce(0, +)
// NotificationCenter
for await notification in NotificationCenter.default.notifications(named: .didUpdate) {
handleUpdate(notification)
}
// URLSession bytes
let (bytes, response) = try await URLSession.shared.bytes(from: url)
for try await byte in bytes {
process(byte)
}
// FileHandle bytes
for try await line in FileHandle.standardInput.bytes.lines {
process(line)
}
| Gotcha | Symptom | Fix |
|---|---|---|
| Continuation yielded after finish | Runtime warning, value lost | Track finished state, guard before yield |
| Stream never finishing | for-await loop hangs forever | Always call continuation.finish() in all code paths |
| No onTermination handler | Resource leak when consumer cancels | Set continuation.onTermination for cleanup |
| Unbounded buffer | Memory growth under load | Use .bufferingNewest(N) or .bufferingOldest(N) |
| Multiple consumers | Only first consumer gets values | AsyncStream is single-consumer; create separate streams per consumer |
| for-await on MainActor | UI freezes waiting for values | Use Task {} to consume off the main path |
@MainActor
func updateUI() {
label.text = "Done"
}
// Call from async context
func doWork() async {
let result = await computeResult()
await updateUI() // Hops to MainActor
}
Explicitly execute a closure on the main actor from any context.
func processData() async {
let result = await heavyComputation()
await MainActor.run {
self.label.text = result
self.progressView.isHidden = true
}
}
Assert that code is already running on the main actor. Crashes at runtime if the assertion is false.
func legacyCallback() {
// We KNOW this is called on main thread (UIKit guarantee)
MainActor.assumeIsolated {
self.viewModel.update() // Access @MainActor state
}
}
See axiom-assume-isolated for comprehensive patterns.
Opt out of the enclosing actor's isolation.
@MainActor
class ViewModel {
let id: UUID // Implicitly nonisolated (let)
nonisolated var analyticsID: String { // Explicitly nonisolated
id.uuidString
}
var items: [Item] = [] // Isolated to MainActor
}
Compiler escape hatch. Tells the compiler to treat a property as if it's not isolated, without any safety guarantees.
// Use only when you have external guarantees of thread safety
nonisolated(unsafe) var legacyState: Int = 0
// Common for global constants that the compiler can't verify
nonisolated(unsafe) let formatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
return f
}()
Warning : nonisolated(unsafe) provides zero runtime protection. Data races will not be caught. Use only as a last resort for bridging legacy code.
Suppress concurrency warnings for pre-concurrency APIs during migration.
// Suppress warnings for entire module
@preconcurrency import MyLegacyFramework
// Suppress for specific protocol conformance
class MyDelegate: @preconcurrency SomeLegacyDelegate {
func delegateCallback() {
// No Sendable warnings for this conformance
}
}
Capture the caller's isolation context so a function runs on whatever actor the caller is on.
func doWork(isolation: isolated (any Actor)? = #isolation) async {
// Runs on caller's actor — no hop if caller is already isolated
performWork()
}
// Called from @MainActor — runs on MainActor
@MainActor
func setup() async {
await doWork() // doWork runs on MainActor
}
// Called from custom actor — runs on that actor
actor MyActor {
func run() async {
await doWork() // doWork runs on MyActor
}
}
| Gotcha | Symptom | Fix |
|---|---|---|
| MainActor.run from MainActor | Unnecessary hop, potential deadlock risk | Check context or use assumeIsolated |
| nonisolated(unsafe) data race | Crash at runtime, corrupted state | Use proper isolation or Mutex |
| @preconcurrency hiding real issues | Runtime crashes in production | Migrate to proper concurrency before shipping |
| #isolation not available pre-5.9 | Compiler error | Use traditional @MainActor annotation |
| nonisolated on actor method | Can't access any isolated state | Only use for computed properties from non-isolated state |
Bridge callback-based APIs to async/await.
Non-throwing bridge.
func currentLocation() async -> CLLocation {
await withCheckedContinuation { continuation in
locationManager.requestLocation { location in
continuation.resume(returning: location)
}
}
}
Throwing bridge.
func fetchUser(id: String) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
api.fetchUser(id: id) { result in
switch result {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
// Return a value
continuation.resume(returning: value)
// Throw an error
continuation.resume(throwing: error)
// From a Result type
continuation.resume(with: result) // Result<T, Error>
A continuation MUST be resumed exactly once:
Resuming twice crashes with "Continuation already resumed" (checked) or undefined behavior (unsafe)
Never resuming causes the awaiting task to hang forever — a silent leak
// DANGEROUS: callback might not be called func riskyBridge() async throws -> Data { try await withCheckedThrowingContinuation { continuation in api.fetch { data, error in if let error { continuation.resume(throwing: error) return } if let data { continuation.resume(returning: data) return } // BUG: if both are nil, continuation is never resumed // Fix: add a fallback continuation.resume(throwing: BridgeError.noResponse) } } }
class LocationBridge: NSObject, CLLocationManagerDelegate {
private var continuation: CheckedContinuation<CLLocation, Error>?
private let manager = CLLocationManager()
func requestLocation() async throws -> CLLocation {
try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
manager.delegate = self
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
continuation?.resume(returning: locations[0])
continuation = nil // Prevent double resume
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
continuation?.resume(throwing: error)
continuation = nil
}
}
Skip runtime checks for performance. Same API as checked, but misuse causes undefined behavior instead of a diagnostic crash.
func fastBridge() async -> Data {
await withUnsafeContinuation { continuation in
// No runtime check for double-resume or missing resume
fastCallback { data in
continuation.resume(returning: data)
}
}
}
Use checked continuations during development, switch to unsafe only after thorough testing and when profiling shows the check is a bottleneck.
| Gotcha | Symptom | Fix |
|---|---|---|
| Resume called twice | "Continuation already resumed" crash | Set continuation to nil after resume |
| Resume never called | Task hangs indefinitely | Ensure all code paths resume — including error/nil cases |
| Capturing continuation | Continuation escapes scope | Store in property, ensure single resume |
| Unsafe continuation in debug | No diagnostics for misuse | Use withCheckedContinuation during development |
| Delegate called multiple times | Crash on second resume | Use AsyncStream instead of continuation for repeated callbacks |
| Callback on wrong thread | Doesn't matter for continuation | Continuations can be resumed from any thread |
Common migrations from GCD and completion handlers to Swift concurrency.
// BEFORE: DispatchQueue for thread safety
class ImageCache {
private let queue = DispatchQueue(label: "cache", attributes: .concurrent)
private var cache: [URL: UIImage] = [:]
func get(_ url: URL, completion: @escaping (UIImage?) -> Void) {
queue.async { completion(self.cache[url]) }
}
func set(_ url: URL, image: UIImage) {
queue.async(flags: .barrier) { self.cache[url] = image }
}
}
// AFTER: Actor
actor ImageCache {
private var cache: [URL: UIImage] = [:]
func get(_ url: URL) -> UIImage? {
cache[url]
}
func set(_ url: URL, image: UIImage) {
cache[url] = image
}
}
// BEFORE: DispatchGroup
let group = DispatchGroup()
var results: [Data] = []
for url in urls {
group.enter()
fetch(url) { data in
results.append(data)
group.leave()
}
}
group.notify(queue: .main) { use(results) }
// AFTER: TaskGroup
let results = await withTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask { await fetch(url) }
}
var collected: [Data] = []
for await data in group {
collected.append(data)
}
return collected
}
use(results)
// BEFORE
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error { completion(.failure(error)); return }
guard let data else { completion(.failure(FetchError.noData)); return }
completion(.success(data))
}.resume()
}
// AFTER
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
@MainActor
class ViewController: UIViewController, UITableViewDelegate {
// @objc delegate methods inherit @MainActor isolation from the class
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Already on MainActor — safe to update UI
updateSelection(indexPath)
}
}
// BEFORE
let observer = NotificationCenter.default.addObserver(
forName: .didUpdate, object: nil, queue: .main
) { notification in
handleUpdate(notification)
}
// Must remove observer in deinit
// AFTER
let task = Task {
for await notification in NotificationCenter.default.notifications(named: .didUpdate) {
await handleUpdate(notification)
}
}
// Cancel task in deinit — no manual observer removal needed
// BEFORE
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
updateUI()
}
// Must invalidate in deinit
// AFTER
let task = Task {
while !Task.isCancelled {
await updateUI()
try? await Task.sleep(for: .seconds(1))
}
}
// Cancel task in deinit
// BEFORE: Semaphore to limit concurrent operations
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
DispatchQueue.global().async {
semaphore.wait()
defer { semaphore.signal() }
download(url)
}
}
// AFTER: TaskGroup with limited concurrency
await withTaskGroup(of: Void.self) { group in
var inFlight = 0
for url in urls {
if inFlight >= 3 {
await group.next() // Wait for one to finish
inFlight -= 1
}
group.addTask { await download(url) }
inFlight += 1
}
await group.waitForAll()
}
| Gotcha | Symptom | Fix |
|---|---|---|
| DispatchQueue.sync to actor | Deadlock potential | Remove .sync, use await |
| Global dispatch to actor contention | Slowdown from serialization | Profile with Concurrency Instruments |
| Legacy delegate + Sendable | "Cannot conform to Sendable" | Use @preconcurrency import or @MainActor isolation |
| Callback called multiple times | Continuation crash | Use AsyncStream instead of continuation |
| Semaphore.wait in async context | Thread starvation, potential deadlock | Use TaskGroup with manual concurrency limiting |
| DispatchQueue.main.async to MainActor | Subtle timing differences | MainActor.run is the equivalent — test edge cases |
| Task | API | Swift Version |
|---|---|---|
| Define isolated type | actor MyActor { } | 5.5+ |
| Run on main thread | @MainActor | 5.5+ |
| Mark as safe to share | : Sendable | 5.5+ |
| Mark closure safe to share | @Sendable | 5.5+ |
| Parallel tasks (fixed) | async let | 5.5+ |
WWDC : 2021-10132, 2021-10134, 2022-110350, 2025-268
Docs : /swift/concurrency, /swift/actor, /swift/sendable, /swift/taskgroup
Skills : swift-concurrency, assume-isolated, synchronization, concurrency-profiling
Weekly Installs
25
Repository
GitHub Stars
601
First Seen
12 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode24
github-copilot24
codex24
kimi-cli24
gemini-cli24
cursor24
| Parallel tasks (dynamic) | withTaskGroup | 5.5+ |
| Stream values | AsyncStream | 5.5+ |
| Bridge callback | withCheckedContinuation | 5.5+ |
| Check cancellation | Task.checkCancellation() | 5.5+ |
| Task-scoped values | @TaskLocal | 5.5+ |
| Assert isolation | MainActor.assumeIsolated | 5.9+ (iOS 17+) |
| Capture caller isolation | #isolation | 5.9+ |
| Lock-based sync | Mutex | 6.0+ (iOS 18+) |
| Discard results | withDiscardingTaskGroup | 5.9+ (iOS 17+) |
| Transfer ownership | sending parameter | 6.0+ |
| Force background | @concurrent | 6.2+ |
| Isolated conformance | extension: @MainActor Proto | 6.2+ |