《SWIFTER Swift 异步和并发》学习笔记

2 阅读8分钟

2. Swift 并发初步

一些基本概念

同步和异步

  • 同步(Synchronous):函数调用后立即执行所有操作,直到任务完成才返回结果,阻塞当前线程。适合耗时极短、必须立即完成的操作。
  • 异步(Asynchronous):函数调用后不等待任务完成,立即返回;任务在后台执行,完成后通过回调、async/await等方式通知结果,不阻塞当前线程。适合网络请求、文件读写、复杂计算等耗时操作。
import Foundation

// 同步函数:阻塞当前线程直到返回
func synchronousLoadData(from url: URL) -> Data? {
    return try? Data(contentsOf: url)
}

// 异步函数(async/await):不阻塞当前线程
func asynchronousLoadData(from url: URL) async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

// 使用示例
func testSyncAsync() {
    let url = URL(string: "https://example.com")!
    
    // 同步调用:会阻塞主线程
    let syncData = synchronousLoadData(from: url)
    print("同步数据长度:\(syncData?.count ?? 0)")
    
    // 异步调用:必须在异步上下文中执行
    Task {
        do {
            let asyncData = try await asynchronousLoadData(from: url)
            print("异步数据长度:\(asyncData.count)")
        } catch {
            print("请求失败:\(error)")
        }
    }
}

串行和并行

  • 串行(Serial):多个任务按严格的先后顺序执行,前一个任务完全完成后,后一个才开始。同一时间只有一个任务在运行。
  • 并行(Parallel):多个任务同时执行,依赖多核CPU硬件支持。同一时间有多个任务在不同CPU核心上运行。
// 模拟耗时任务
func work(_ number: Int) async {
    print("开始任务 \(number)")
    try? await Task.sleep(for: .seconds(1))
    print("完成任务 \(number)")
}

// 串行执行:总耗时约3秒
func serialExecution() async {
    print("=== 串行执行 ===")
    await work(1)
    await work(2)
    await work(3)
}

// 并行执行:总耗时约1秒
func parallelExecution() async {
    print("=== 并行执行 ===")
    async let w1 = work(1)
    async let w2 = work(2)
    async let w3 = work(3)
    await w1, w2, w3
}

// 运行测试
Task {
    await serialExecution()
    await parallelExecution()
}

并发是什么

  • 并发(Concurrency):在同一时间段内处理多个任务的能力。宏观上看起来多个任务同时进行,微观上分为两种实现:
    • 单核CPU:通过时间片轮转切换任务,实现"伪并行"
    • 多核CPU:部分任务真正并行执行
  • 核心目标:提高CPU、内存等资源利用率,提升程序响应速度。
  • 并发 vs 并行:并行是并发的一种实现方式;并发关注"任务的调度和管理",并行关注"任务的同时执行"。

异步函数

核心概念

  • 异步函数:用async标记的函数,内部可以包含暂停点(await)。执行到暂停点时会暂时放弃当前线程,让线程执行其他任务,等待异步操作完成后再恢复执行。
  • await:标记异步函数的暂停点,必须在async函数或async闭包中使用。
  • 续体(Continuation):异步函数暂停时保存的执行状态(包括局部变量、程序计数器等),用于恢复执行。
  • 回调转异步:使用withCheckedContinuation/withUnsafeContinuation将传统基于回调的API转换为async/await风格。
  • 异步序列:表示一系列未来会产生的值,通过for try await迭代,对应同步世界的Sequence

代码示例

  1. 异步函数的链式调用
// 定义异步函数
func fetchUser(id: Int) async throws -> String {
    try await Task.sleep(for: .seconds(1)) // 模拟网络请求
    return "用户\(id)"
}

func fetchUserPosts(userId: String) async throws -> [String] {
    try await Task.sleep(for: .seconds(1))
    return ["帖子1", "帖子2", "帖子3"]
}

// 链式调用异步函数
func loadUserAndPosts() async {
    do {
        let user = try await fetchUser(id: 1)
        let posts = try await fetchUserPosts(userId: user)
        print("用户:\(user),帖子:\(posts)")
    } catch {
        print("加载失败:\(error)")
    }
}

Task {
    await loadUserAndPosts()
}
  1. 传统回调转async/await
// 传统基于回调的API
func legacyFetchData(completion: @escaping (Data?, Error?) -> Void) {
    URLSession.shared.dataTask(with: URL(string: "https://example.com")!) { data, _, error in
        completion(data, error)
    }.resume()
}

// 转换为async/await风格
func modernFetchData() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        legacyFetchData { data, error in
            if let error = error {
                continuation.resume(throwing: error)
            } else if let data = data {
                continuation.resume(returning: data)
            }
        }
    }
}
  1. 异步序列与AsyncStream
// 使用AsyncStream创建计时器异步序列
let timerStream = AsyncStream<Date> { continuation in
    let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
        continuation.yield(Date())
    }
    // 序列结束时清理资源
    continuation.onTermination = { _ in
        timer.invalidate()
    }
}

// 迭代异步序列
Task {
    for await date in timerStream.prefix(3) {
        print("当前时间:\(date)")
    }
    print("计时器序列结束")
}

结构化并发

核心概念

  • 结构化并发:并发任务具有明确的层级关系生命周期,父任务必须等待所有子任务完成后才能结束,取消操作会自动向下传递给所有子任务。
  • 结构化任务创建方式:
    1. async let:异步绑定,适合固定数量的并发任务
    2. TaskGroup:任务组,适合动态数量的并发任务
  • 非结构化任务:Task.init(继承当前上下文)和Task.detached(完全独立),生命周期不与父任务绑定,需要手动管理取消。
  • 协作式取消:任务不会被强制终止,需要主动检查Task.isCancelled或调用Task.checkCancellation()来响应取消。

代码示例

  1. async let 异步绑定
// 同时获取用户和帖子,总耗时约1秒而非2秒
func fetchUserAndPostsParallel() async throws -> (String, [String]) {
    async let user = fetchUser(id: 1)
    async let posts = fetchUserPosts(userId: "用户1")
    return try await (user, posts)
}

Task {
    do {
        let (user, posts) = try await fetchUserAndPostsParallel()
        print("用户:\(user),帖子数:\(posts.count)")
    } catch {
        print("获取失败:\(error)")
    }
}
  1. TaskGroup 动态任务组
// 动态并发获取多个用户
func fetchMultipleUsers(ids: [Int]) async throws -> [String] {
    try await withThrowingTaskGroup(of: String.self) { group in
        // 为每个ID添加子任务
        for id in ids {
            group.addTask {
                try await fetchUser(id: id)
            }
        }
        
        // 收集所有子任务结果
        var users: [String] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

Task {
    let users = try await fetchMultipleUsers(ids: [1, 2, 3])
    print("所有用户:\(users)")
}
  1. 任务取消处理
func longRunningTask() async {
    for i in 1...10 {
        // 主动检查任务是否被取消
        guard !Task.isCancelled else {
            print("任务被取消,提前退出")
            return
        }
        print("执行步骤 \(i)")
        try? await Task.sleep(for: .seconds(0.5))
    }
    print("任务全部完成")
}

Task {
    let task = Task {
        await longRunningTask()
    }
    
    // 2秒后取消任务
    try await Task.sleep(for: .seconds(2))
    task.cancel()
}

actor 模型和数据隔离

核心概念

  • 数据竞争:多个线程同时读写同一块可变内存,导致未定义行为(崩溃、数据错乱等)。
  • actor:Swift原生提供的并发安全类型,通过隔离域保护内部可变状态,同一时间只有一个任务能访问actor的隔离域。
  • 跨actor调用:访问actor的属性或方法必须使用await,会触发"actor跳跃"(切换到actor的专属执行器)。
  • 全局actor:如@MainActor,将代码隔离到主线程执行,专门用于UI相关操作。
  • actor可重入性:actor的异步方法在await暂停时,允许其他任务进入actor执行,可能导致状态在暂停前后发生变化。

代码示例

  1. 基础actor使用(解决数据竞争)
// 定义actor:保护内部计数器
actor SafeCounter {
    private var count = 0
    
    func increment() {
        count += 1
    }
    
    func getCount() -> Int {
        count
    }
}

Task {
    let counter = SafeCounter()
    
    // 1000个并发任务同时递增,无数据竞争
    await withTaskGroup(of: Void.self) { group in
        for _ in 1...1000 {
            group.addTask {
                await counter.increment()
            }
        }
    }
    
    let finalCount = await counter.getCount()
    print("最终计数:\(finalCount)") // 稳定输出1000
}
  1. @MainActor 主线程隔离
import UIKit

// 整个类的所有成员都隔离在主线程
@MainActor
class UserViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        loadUserName()
    }
    
    func loadUserName() {
        Task {
            // 后台加载数据
            let user = try await fetchUser(id: 1)
            // 自动在主线程更新UI,无需手动切换
            nameLabel.text = user
        }
    }
}
  1. actor可重入性问题与解决
actor UserLoader {
    private var cachedUser: String?
    private var isLoading = false
    
    // 问题版本:多个任务同时调用会重复请求
    func badLoadUser() async -> String {
        if let user = cachedUser {
            return user
        }
        
        // await暂停时,其他任务可以进入actor
        try? await Task.sleep(for: .seconds(1))
        let user = "用户1"
        cachedUser = user
        return user
    }
    
    // 修复版本:添加加载状态避免重复请求
    func goodLoadUser() async -> String {
        if let user = cachedUser {
            return user
        }
        
        guard !isLoading else {
            // 等待其他加载任务完成
            while isLoading {
                try? await Task.sleep(for: .milliseconds(100))
            }
            return cachedUser!
        }
        
        isLoading = true
        defer { isLoading = false }
        
        try? await Task.sleep(for: .seconds(1))
        let user = "用户1"
        cachedUser = user
        return user
    }
}

小结

本章是Swift并发编程的基石,核心围绕**"如何安全、高效地处理多任务"**展开:

  1. 基础概念:明确了同步/异步(执行时机)、串行/并行(执行方式)、并发(任务调度)的本质区别,建立并发编程的思维模型。
  2. 异步函数:通过async/await彻底解决了传统回调地狱问题,让异步代码拥有和同步代码相似的可读性;异步序列则统一了"一系列未来值"的处理方式。
  3. 结构化并发:通过层级化的任务管理,自动处理任务生命周期、错误传播和取消操作,从根本上避免了传统并发中的资源泄漏、任务逃逸等问题。
  4. actor模型:从语言层面彻底解决了数据竞争问题,通过隔离域保证可变状态的安全访问;@MainActor等全局actor则简化了主线程UI操作的安全管理。

这些概念相互配合,构成了Swift并发编程的完整框架,让开发者能够以更简洁、更安全的方式编写高性能的并发代码。

需要我把这些知识点整理成一份可打印的速查表,方便你随时查阅吗?