Swift 并发核心概念与实战指南

Viewed 0

采用 Swift 并发

与我们一起了解 Swift 并发方面的核心概念。并发可帮助你提高 App 的响应速度和性能,而 Swift 旨在让正确编写异步和并发代码变得更加简单。我们会介绍将 App 编程从单线程转换为并发流程所需的步骤。无论你想让代码更加异步化、将代码移到后台,还是在并发任务中共享数据,我们都将帮助你确定充分利用 Swift 并发功能的恰当方式和时机。

简介

Swift 并发使 App 能够同时执行多个任务,从而提升响应速度并将计算任务转移到后台。Swift 的并发模型通过显式引入并发、识别并发任务之间共享的数据以及在编译时识别潜在的数据争用,使正确编写并发代码变得更容易。

App 会首先在主线程上运行所有代码。随着复杂性增加,可以引入异步任务来执行网络访问等高延迟操作。后台线程可用于执行计算密集型任务。Swift 提供了 Actor 和 Task 等工具来表达这些并发操作。

单线程代码

在 Swift 中,单线程代码在主线程上运行,主线程与主 Actor 分离。主 Actor 上没有并发,因为只有一个主线程可以运行它。你可以使用 @MainActor 注释来指定主 Actor 上的数据或代码。

Swift 将确保主 Actor 代码只在主线程上运行,并且只能从主线程访问主 Actor 数据。我们称此类代码与主 Actor 隔离。默认情况下,Swift 使用主 Actor 保护主线程代码,从而确保可以自由访问共享状态。

默认情况下,使用主 Actor 保护代码的功能由构建设置驱动。这主要用于你的 App 主模块以及任何专注于 UI 交互的模块。

示例:单线程程序

var greeting = "Hello, World!"

func readArguments() { }

func greet() {
  print(greeting)
}

readArguments()
greet()

异步任务

Swift 并发使用 async 和 await 将函数设为非阻塞函数,从而允许在等待数据 (如网络请求) 的同时运行其他工作。这可以通过将函数分解为在等待事件之前和之后运行的部分,来防止挂起并提高 UI 响应速度。

交错执行

异步函数在一个任务中运行,将独立于其他任务执行。单线程可以在独立任务就绪后将它们交替运行,也就是使用“交错”机制。这样可以通过避免空闲时间来提高性能,并高效利用系统资源。

当同时执行多个独立操作时,多个异步任务处于有效状态。当按特定顺序执行工作时,请使用单个任务。通常在单线程中使用异步任务就足够了。如果主线程负担过重,Instruments 等性能分析工具可以帮助在引入并发之前识别出需要优化的瓶颈。

并发简介

并发允许部分代码在后台线程上与主线程并行运行,通过使用系统中的更多 CPU 内核来更快地完成工作。为了提高性能,这里的示例引入了并发以在后台线程上运行代码,从而释放主线程。

并发函数

应用 @concurrent 属性,就可以指示 Swift 在后台运行一个函数。Swift 编译器会突出显示主 Actor 上的数据访问,以安全地引入并发。为了确保工作同步进行,一种最佳实践是将主 Actor 代码移至始终在主线程上运行的调用程序中。

示例:异步网络请求和图像解码

import Foundation

class Image {
}

final class View {
    func displayImage(_ image: Image) {
    }
}

final class ImageModel {
    var imageCache: [URL: Image] = [:]
    let view = View()

    func fetchAndDisplayImage(url: URL) async throws {
      let (data, _) = try await URLSession.shared.data(from: url)
      let image = decodeImage(data)
      view.displayImage(image)
    }

    func decodeImage(_ data: Data) -> Image {
      Image()
    }
}

final class Library {
    static let shared: Library = Library()
}

非限定代码

@concurrent 函数总是会脱离 Actor 运行。“nonisolated”关键字允许客户端选择运行代码的位置 (main 或 background)。对于通用资源库,建议提供非隔离的 API,并让客户端自行决定是否转移工作。有关更多并发选项,请参阅 WWDC23 中的“超越结构化并发的基础”。

并发线程池

将工作转移到后台时,系统会管理在并发线程池中的线程上的工作调度。较小的设备可能在池中拥有较少的线程,而具有较多内核的较大系统则将具有较多线程。任务会分配给池中的可用线程,这些线程可能会随着任务的暂停和恢复而变化,从而优化资源利用率。

共享数据

在使用并发并在不同线程之间共享数据时,访问共享的可变状态可能会引入运行时错误。Swift 的设计将通过提供编译时检查来帮助缓解这个问题,使开发者能够更自信地编写并发代码。

值类型

在处理并发任务时,使用值类型具有显著优势。当将值类型拷贝到后台线程时,会创建一个独立的副本,从而确保在主线程上所做的任何更改都不会影响后台线程的值。这种独立性使得值类型可以安全地在线程之间共享。

符合“Sendable”协议的值类型始终可以安全地并发共享。主 Actor 类型隐式符合 Sendable。

示例:值类型和 Sendable

import Foundation

// Value types are common in Swift
struct Post {
    var author: String
    var title: String
    var date: Date
    var categories: [String]
}

// Value types are Sendable
extension URL: Sendable {}

// Collections of Sendable elements
extension Array: Sendable where Element: Sendable {}

// Structs and enums with Sendable storage
struct ImageRequest: Sendable {
    var url: URL
}

// Main-actor types are implicitly Sendable
@MainActor class ImageModel {}

Actor 限定类型

Swift Actor 通过确保单任务访问来保护非 Sendable 状态。当与 Actor 之间互相发送值时,Swift 编译器会验证安全性。

在 Swift 中,类是引用类型,这意味着通过一个变量对对象进行的更改将对所有指向这个对象的变量可见。当多个线程同时访问和修改同一个非 Sendable 对象时,可能会导致数据争用、崩溃或故障。Swift 的并发系统会强制仅在 Actor 和 Task 之间共享 Sendable 类型,从而防止在编译时发生这种情况。

为了避免数据争用,确保可变对象不被并发共享至关重要。在将对象发送到另一个 Task 或 Actor 进行显示或处理之前,请先完成对对象的修改。如果需要在后台线程上修改对象,请将它设置为“nonisolated”状态,但不要设置为 Sendable。具有共享状态的闭包只要不被并发调用,也可以是安全的。

示例:类和非 Sendable 对象

import Foundation

struct Color { }

nonisolated class MyImage {
    var width: Int
    var height: Int
    var pixels: [Color]
    var url: URL

    init() {
      width = 100
      height = 100
      pixels = []
      url = URL("https://swift.org")!
    }

    func scale(by factor: Double) {
    }
}

let image = MyImage()
let otherImage = image // refers to the same object as 'image'
image.scale(by: 0.5)   // also changes otherImage!

Actor

随着 App 的发展,主 Actor 可能会管理大量状态,从而导致频繁的上下文切换。引入 Actor 可以缓解这种情况。Actor 会隔离这些数据,一次只允许一个线程进行访问,从而避免争用。将状态从主 Actor 转移到专用 Actor,就可以在后台线程上并发执行更多代码。这样可以释放主线程,以确保响应能力。面向 UI 的类和模型类通常需要保留在主 Actor 上,或者保持非 Sendable 状态不变。

示例:Network manager 作为 Actor

import Foundation

nonisolated class MyImage { }

struct Connection {
    func data(from url: URL) async throws -> Data { Data() }
}

actor NetworkManager {
    var openConnections: [URL: Connection] = [:]

    func openConnection(for url: URL) async -> Connection {
      if let connection = openConnections[url] {
        return connection
      }

      let connection = Connection()
      openConnections[url] = connection
      return connection
    }

    func closeConnection(_ connection: Connection, for url: URL) async {
      openConnections.removeValue(forKey: url)
    }

}

final class View {
    func displayImage(_ image: MyImage) {
    }
}

final class ImageModel {
    var cachedImage: [URL: MyImage] = [:]
    let view = View()
    let networkManager: NetworkManager = NetworkManager()

    func fetchAndDisplayImage(url: URL) async throws {
      if let image = cachedImage[url] {
        view.displayImage(image)
        return
      }

      let connection = await networkManager.openConnection(for: url)
      let data = try await connection.data(from: url)
      await networkManager.closeConnection(connection, for: url)

      let image = await decodeImage(data)
      view.displayImage(image)
    }

    @concurrent
    func decodeImage(_ data: Data) async -> MyImage {
      // decode image
      return MyImage()
    }
}

总结

App 通常最初是单线程的,然后逐渐演变为使用异步任务、并发代码和 Actor 以提升性能。Swift 并发有助于实现这一转变,使代码更容易从主线程移出并提升响应速度。Instruments 等性能分析工具有助于识别何时以及哪些代码需要从主线程移出。请使用推荐的构建设置来帮助简化并发的引入,并使用“渐进式并发”设置实现一系列即将推出的功能,从而更轻松地使用并发。

0 Answers