他妈的易于理解的 Swift 并发编程
Approachable Swift Concurrency

原始链接: https://fuckingapproachableswiftconcurrency.com/en/

## Swift 并发:安全与代码运行位置 大多数应用是 I/O 密集型的,使用 `async/await` 和 `Tasks` 在主线程上处理得很好。然而,CPU 密集型任务(如复杂计算)如果直接在主线程上运行*会*冻结 UI。 历史上,管理这个问题涉及线程、GCD 和 Combine 等选项——所有这些都需要手动安全措施来防止数据竞争(当多个线程同时访问和修改相同的数据时)。数据竞争会导致不可预测的崩溃和错误。 Swift 的并发系统将重点从代码*运行的位置*转移到*谁*可以访问数据,引入了 **隔离**。它建立在 libdispatch 之上,通过 **actor** 添加编译时安全检查。 有三种隔离域:**MainActor**(用于 UI 更新,通过 `@MainActor` 强制执行)、**Actors**(保护可变状态,通过 `await` 访问)和 **nonisolated** 代码(选择退出隔离,可以从任何地方访问)。 新的 Xcode 项目默认采用简化的模型,`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` 和 `SWIFT_APPROACHABLE_CONCURRENCY = YES`,使并发更容易上手。可以使用 `@concurrent` 卸载 CPU 密集型工作。该系统优先考虑安全性,让编译器防止数据竞争,而不是依赖于手动且容易出错的解决方案。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 易于理解的 Swift 并发 (fuckingapproachableswiftconcurrency.com) 11 分,由 wrxd 发表于 59 分钟前 | 隐藏 | 过去 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

So far we've talked about when code runs (async/await) and how to organize it (Tasks). Now: where does it run, and how do we keep it safe?

Most apps just wait

Most app code is I/O-bound. You fetch data from a network, await a response, decode it, and display it. If you have multiple I/O operations to coordinate, you resort to tasks and task groups. The actual CPU work is minimal. The main thread can handle this fine because await suspends without blocking.

But sooner or later, you'll have CPU-bound work: parsing a giant JSON file, processing images, running complex calculations. This work doesn't wait for anything external. It just needs CPU cycles. If you run it on the main thread, your UI freezes. This is where "where does code run" actually matters.

The Old World: Many Options, No Safety

Before Swift's concurrency system, you had several ways to manage execution:

Approach What it does Tradeoffs
Thread Direct thread control Low-level, error-prone, rarely needed
GCD Dispatch queues with closures Simple but no cancellation, easy to cause thread explosion
OperationQueue Task dependencies, cancellation, KVO More control but verbose and heavyweight
Combine Reactive streams Great for event streams, steep learning curve

All of these worked, but safety was entirely on you. The compiler couldn't help if you forgot to dispatch to main, or if two queues accessed the same data simultaneously.

The Problem: Data Races

A data race happens when two threads access the same memory at the same time, and at least one is writing:

var count = 0

DispatchQueue.global().async { count += 1 }
DispatchQueue.global().async { count += 1 }

Data races are undefined behavior. They can crash, corrupt memory, or silently produce wrong results. Your app works fine in testing, then crashes randomly in production. Traditional tools like locks and semaphores help, but they're manual and error-prone.

Concurrency amplifies the problem

The more concurrent your app is, the more likely data races become. A simple iOS app might get away with sloppy thread safety. A web server handling thousands of simultaneous requests will crash constantly. This is why Swift's compile-time safety matters most in high-concurrency environments.

The Shift: From Threads to Isolation

Swift's concurrency model asks a different question. Instead of "which thread should this run on?", it asks: "who is allowed to access this data?"

This is isolation. Rather than manually dispatching work to threads, you declare boundaries around data. The compiler enforces these boundaries at build time, not runtime.

Under the hood

Swift Concurrency is built on top of libdispatch (the same runtime as GCD). The difference is the compile-time layer: actors and isolation are enforced by the compiler, while the runtime handles scheduling on a cooperative thread pool limited to your CPU's core count.

The Three Isolation Domains

1. MainActor

@MainActor is a global actor that represents the main thread's isolation domain. It's special because UI frameworks (UIKit, AppKit, SwiftUI) require main thread access.

@MainActor
class ViewModel {
    var items: [Item] = []  
}

When you mark something @MainActor, you're not saying "dispatch this to the main thread." You're saying "this belongs to the main actor's isolation domain." The compiler enforces that anything accessing it must either be on MainActor or await to cross the boundary.

When in doubt, use @MainActor

For most apps, marking your ViewModels with @MainActor is the right choice. Performance concerns are usually overblown. Start here, optimize only if you measure actual problems.

2. Actors

An actor protects its own mutable state. It guarantees that only one piece of code can access its data at a time:

actor BankAccount {
    var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount  
    }
}


await account.deposit(100)

Actors are not threads. An actor is an isolation boundary. The Swift runtime decides which thread actually executes actor code. You don't control that, and you don't need to.

3. Nonisolated

Code marked nonisolated opts out of actor isolation. It can be called from anywhere without await, but it cannot access the actor's protected state:

actor BankAccount {
    var balance: Double = 0

    nonisolated func bankName() -> String {
        "Acme Bank"  
    }
}

let name = account.bankName()  

Approachable Concurrency: Less Friction

Approachable Concurrency simplifies the mental model with two Xcode build settings:

  • SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor: Everything runs on MainActor unless you say otherwise
  • SWIFT_APPROACHABLE_CONCURRENCY = YES: nonisolated async functions stay on the caller's actor instead of jumping to a background thread

New Xcode 26 projects have both enabled by default. When you need CPU-intensive work off the main thread, use @concurrent.

// Runs on MainActor (the default)
func updateUI() async { }

// Runs on background thread (opt-in)
@concurrent func processLargeFile() async { }

The Office Building

Think of your app as an office building. Each isolation domain is a private office with a lock on the door. Only one person can be inside at a time, working with the documents in that office.

  • MainActor is the front desk - where all customer interactions happen. There's only one, and it handles everything the user sees.
  • actor types are department offices - Accounting, Legal, HR. Each protects its own sensitive documents.
  • nonisolated code is the hallway - shared space anyone can walk through, but no private documents live there.

You can't just barge into someone's office. You knock (await) and wait for them to let you in.

联系我们 contact @ memedata.com