Swift 异步并发学习

Swift 异步并发学习

一、基础概念

  1. 异步编程简介
  • 同步与异步的区别
  • 为何需要异步编程
  1. 并发概念理解
  • 并发与并行的差异
  • 线程、队列与任务的关系

二、Swift 异步编程基础

  1. 闭包与回调
  • 用闭包实现简单异步操作
  • 回调地狱问题及解决思路
  1. 函数式编程基础
  • 高阶函数(如 map、filter、reduce)在异步中的应用
  • 理解函数柯里化与异步操作组合

闭包与回调

  1. 用闭包实现简单异步操作
func performNetworkRequest(completion: @escaping (Data?, Error?) -> Void) {    let url = URL(string: "https://example.com/api/data")!    URLSession.shared.dataTask(with: url) { (data, response, error) in        completion(data, error)    }.resume()}performNetworkRequest { (data, error) in    if let error = error {        print("Network error: \(error)")    } else if let data = data {        // 处理接收到的数据        let responseString = String(data: data, encoding:.utf8)        print("Response: \(responseString?? "Empty response")")    }}
  • 在 Swift 中,闭包是一种自包含的代码块,可以作为参数传递给函数,也可以作为函数的返回值。利用闭包,我们能方便地实现异步操作。例如,在进行网络请求时,可将数据处理逻辑放在闭包中,待请求完成后执行。
  1. 回调地狱问题及解决思路
func stepOne(completion: @escaping (Result<Data, Error>) -> Void) {    // 模拟异步操作    DispatchQueue.global().asyncAfter(deadline:.now() + 1) {        let result: Result<Data, Error> =.success(Data())        completion(result)    }}func stepTwo(data: Data, completion: @escaping (Result<Data, Error>) -> Void) {    // 模拟异步操作    DispatchQueue.global().asyncAfter(deadline:.now() + 1) {        let result: Result<Data, Error> =.success(Data())        completion(result)    }}func stepThree(data: Data, completion: @escaping (Result<Data, Error>) -> Void) {    // 模拟异步操作    DispatchQueue.global().asyncAfter(deadline:.now() + 1) {        let result: Result<Data, Error> =.success(Data())        completion(result)    }}stepOne { result in    switch result {    case.success(let data1):        stepTwo(data: data1) { result in            switch result {            case.success(let data2):                stepThree(data: data2) { result in                    switch result {                    case.success:                        print("All steps completed successfully")                    case.failure(let error):                        print("Error in step three: \(error)")                    }                }            case.failure(let error):                print("Error in step two: \(error)")            }        }    case.failure(let error):        print("Error in step one: \(error)")    }}
  • 当有多个异步操作依赖时,使用传统回调方式容易导致代码嵌套层数过多,形成所谓的 “回调地狱”,使代码可读性和维护性变差。例如:
  • 解决回调地狱的思路有多种,如使用链式调用、将回调函数封装成类或结构体、利用 Swift 的Result类型简化错误处理等。以链式调用为例,可以定义一个包含所有步骤的类,每个步骤返回自身,从而实现链式操作。

函数式编程基础

  1. 高阶函数(如 map、filter、reduce)在异步中的应用
func fetchUserIds(completion: @escaping (Result<[Int], Error>) -> Void) {    // 模拟异步网络请求返回用户ID数组    DispatchQueue.global().asyncAfter(deadline:.now() + 1) {        let result: Result<[Int], Error> =.success([1, 2, 3])        completion(result)    }}func fetchUserById(id: Int, completion: @escaping (Result<User, Error>) -> Void) {    // 模拟异步网络请求根据ID获取用户    DispatchQueue.global().asyncAfter(deadline:.now() + 1) {        let user = User(id: id, name: "User\(id)")        let result: Result<User, Error> =.success(user)        completion(result)    }}fetchUserIds { result in    switch result {    case.success(let userIds):        let userFetchTasks = userIds.map { id in            return { completion in                fetchUserById(id: id, completion: completion)            }        }        // 这里可以并发执行userFetchTasks中的任务    case.failure(let error):        print("Error fetching user IDs: \(error)")    }}
  • map函数可用于对异步操作的结果进行转换。例如,从网络请求获取到一组数据后,使用map将数据模型转换为另一种形式。
  • filter函数可用于筛选异步操作结果。比如在获取到一组用户数据后,筛选出特定条件的用户。
  • reduce函数可用于对异步操作结果进行累积计算。例如,对一组异步获取的数值进行求和。
  1. 理解函数柯里化与异步操作组合
func asyncFunction(a: Int, b: Int, completion: @escaping (Result<Int, Error>) -> Void) {    // 模拟异步计算a + b    DispatchQueue.global().asyncAfter(deadline:.now() + 1) {        let result: Result<Int, Error> =.success(a + b)        completion(result)    }}func curryAsyncFunction(a: Int) -> (Int, @escaping (Result<Int, Error>) -> Void) -> Void {    return { b, completion in        asyncFunction(a: a, b: b, completion: completion)    }}let curriedFunction = curryAsyncFunction(a: 5)curriedFunction(3) { result in    switch result {    case.success(let sum):        print("Sum: \(sum)")    case.failure(let error):        print("Error: \(error)")    }}
  • 函数柯里化是将一个多参数函数转换为一系列单参数函数的过程。在异步编程中,函数柯里化有助于更好地组合异步操作。例如,假设有一个需要两个参数的异步函数asyncFunction(a: Int, b: Int, completion: @escaping (Result<Int, Error>) -> Void),通过柯里化可以先固定一个参数,得到一个新的单参数异步函数。
  • 这样,在需要多次使用不同b值与固定a值进行异步计算时,通过柯里化可以更方便地组合操作,提高代码的复用性和灵活性。

三、Swift 并发模型

  1. GCD(Grand Central Dispatch)
  • 调度队列(串行、并行、主队列)
  • 任务提交(同步、异步)
  • 组队列与 Barrier 任务
  1. OperationQueue
  • 任务(NSOperation)的创建与配置
  • 操作依赖关系设置
  • 队列优先级与最大并发数调整

GCD(Grand Central Dispatch)

  1. 调度队列(串行、并行、主队列)
let serialQueue = DispatchQueue(label: "com.example.serialQueue")serialQueue.async {    // 任务1    print("Task 1 in serial queue")}serialQueue.async {    // 任务2    print("Task 2 in serial queue")}

上述代码中,任务 1 先被添加到串行队列,执行完任务 1 后才会执行任务 2。

let globalQueue = DispatchQueue.global(qos:.default)globalQueue.async {    // 任务3    print("Task 3 in global parallel queue")}globalQueue.async {    // 任务4    print("Task 4 in global parallel queue")}let customConcurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes:.concurrent)customConcurrentQueue.async {    // 任务5    print("Task 5 in custom concurrent queue")}customConcurrentQueue.async {    // 任务6    print("Task 6 in custom concurrent queue")}

任务 3、任务 4、任务 5 和任务 6 可能会并发执行。

DispatchQueue.main.async {    // 更新UI    view.backgroundColor =.red}
  • 串行队列:串行队列会按照任务添加的顺序依次执行任务,同一时间只有一个任务在执行。可通过DispatchQueue(label: String)创建自定义串行队列。例如:
  • 并行队列:并行队列能同时执行多个任务,具体并发数量由系统根据设备性能和资源动态调整。可通过DispatchQueue.global(qos:.default)获取全局并行队列,也可通过DispatchQueue(label: String, attributes:.concurrent)创建自定义并行队列。示例如下:
  • 主队列:主队列与主线程相关联,在主队列中执行的任务会在主线程中运行。通常用于更新 UI 等必须在主线程执行的操作。可通过DispatchQueue.main获取主队列。例如:
  1. 任务提交(同步、异步)
let serialQueue = DispatchQueue(label: "com.example.serialQueue")print("Before sync task")serialQueue.sync {    // 模拟耗时任务    sleep(2)    print("Sync task completed")}print("After sync task")

输出结果为:

Before sync taskSync task completedAfter sync task

可以看到,在同步任务执行期间,主线程被阻塞。

  • 同步提交:使用DispatchQueue.sync方法提交任务时,当前线程会等待任务执行完毕才会继续执行后续代码。例如:
  • 异步提交:通过DispatchQueue.async方法提交任务时,任务会被添加到队列中,当前线程不会等待任务执行,而是继续执行后续代码。如前面创建队列的示例中,大多使用async方法提交任务,使任务异步执行,不阻塞当前线程。
  1. 组队列与 Barrier 任务
let group = DispatchGroup()let queue = DispatchQueue(label: "com.example.groupQueue")group.enter()queue.async {    // 任务7    print("Task 7 in group queue")    group.leave()}group.enter()queue.async {    // 任务8    print("Task 8 in group queue")    group.leave()}group.notify(queue:.main) {    print("All tasks in group completed")}

这里通过group.enter()和group.leave()标记任务的开始和结束,group.notify方法在所有任务完成后执行。

let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes:.concurrent)concurrentQueue.async {    // 任务9    print("Task 9 in concurrent queue")}concurrentQueue.async(flags:.barrier) {    // Barrier任务    print("Barrier task in concurrent queue")}concurrentQueue.async {    // 任务10    print("Task 10 in concurrent queue")}

任务 9 可能在 Barrier 任务之前或之后执行,但任务 10 一定会在 Barrier 任务执行完毕后才开始执行。

  • 组队列:可以使用DispatchGroup来创建组队列,用于跟踪一组任务的完成情况。例如:
  • Barrier 任务:在并行队列中,Barrier 任务会等待队列中已有的任务执行完毕,然后独占队列执行自身任务,执行完成后其他任务才继续执行。可通过DispatchQueue.async(flags:.barrier)提交 Barrier 任务。例如:

OperationQueue

  1. 任务(NSOperation)的创建与配置
let blockOperation = NSBlockOperation {    // 任务代码    print("NSBlockOperation task")}

也可以自定义NSOperation子类:

class CustomOperation: NSOperation {    override func main() {        if!isCancelled {            // 任务逻辑            print("CustomOperation task")        }    }}let customOperation = CustomOperation()

任务创建后,可以配置任务的优先级、依赖关系等属性。例如设置任务优先级:

blockOperation.queuePriority =.high
  • NSOperation是一个抽象类,用于表示一个可执行的任务。可以通过创建其子类NSBlockOperation或自定义子类来创建任务。例如,使用NSBlockOperation创建任务:
  1. 操作依赖关系设置

可以通过addDependency(_:)方法设置任务之间的依赖关系。例如:

let operation1 = NSBlockOperation {    print("Operation 1")}let operation2 = NSBlockOperation {    print("Operation 2")}let operation3 = NSBlockOperation {    print("Operation 3")}operation2.addDependency(operation1)operation3.addDependency(operation2)let operationQueue = OperationQueue()operationQueue.addOperations([operation1, operation2, operation3], waitUntilFinished: false)

上述代码中,operation2 依赖 operation1,operation3 依赖 operation2,所以执行顺序为 operation1 -> operation2 -> operation3。

  1. 队列优先级与最大并发数调整
let operationQueue = OperationQueue()operationQueue.queuePriority =.high
let operationQueue = OperationQueue()operationQueue.maxConcurrentOperationCount = 2

这意味着该队列最多同时执行 2 个任务。

  • 队列优先级:OperationQueue有不同的优先级设置,可以通过queuePriority属性调整队列中任务的执行优先级。例如:
  • 最大并发数调整:通过maxConcurrentOperationCount属性可以设置队列中最大并发执行的任务数量。
  1. 概念与作用:OperationQueue的maxConcurrentOperationCount属性决定了该队列中能够同时执行的最大任务数量。这一属性在管理资源和控制任务执行节奏方面起着关键作用。例如,当处理大量图片下载任务时,如果同时并发下载过多图片,可能会耗尽网络带宽或设备内存资源,导致应用卡顿甚至崩溃。通过设置合理的最大并发数,可以平衡资源利用和任务执行效率。
  2. 设置方式:可以直接在OperationQueue实例上设置maxConcurrentOperationCount。例如:
let operationQueue = OperationQueue()operationQueue.maxConcurrentOperationCount = 3

上述代码将operationQueue的最大并发数设置为 3,意味着该队列最多同时执行 3 个任务。当队列中有超过 3 个任务时,其余任务会在队列中等待,直到有正在执行的任务完成,空出执行名额。

  1. 对任务执行的影响:假设我们有一个包含 10 个任务的OperationQueue,且最大并发数设置为 3。任务会按照添加到队列的顺序,每次最多 3 个任务同时执行。例如:
let operationQueue = OperationQueue()operationQueue.maxConcurrentOperationCount = 3for i in 1...10 {    let operation = NSBlockOperation {        print("Task \(i) is running on thread \(Thread.current)")        sleep(1) // 模拟任务执行时间        print("Task \(i) completed")    }    operationQueue.addOperation(operation)}

在这个例子中,任务 1、任务 2 和任务 3 会首先开始执行,当其中一个任务完成后,任务 4 会立即开始执行,始终保持最多 3 个任务并发执行,直到所有 10 个任务都完成。

OperationQueue 的队列优先级

  1. 概念与作用:OperationQueue的queuePriority属性用于设置队列的整体优先级。优先级高的队列中的任务会比优先级低的队列中的任务更优先获得执行机会。这在处理多种类型任务时非常有用,例如在一个应用中,用户界面更新任务的优先级通常较高,需要及时响应用户操作,而一些后台数据同步任务优先级可以相对较低。
  2. 优先级级别:queuePriority有多个级别,包括.veryHigh、.high、.normal(默认级别)、.low和.veryLow。例如:
let highPriorityQueue = OperationQueue()highPriorityQueue.queuePriority =.highlet lowPriorityQueue = OperationQueue()lowPriorityQueue.queuePriority =.low
  1. 对任务执行顺序的影响:当系统资源有限,多个队列竞争执行机会时,优先级高的队列中的任务会优先执行。例如,假设有两个队列highPriorityQueue和lowPriorityQueue,分别添加了多个任务:
let highPriorityQueue = OperationQueue()highPriorityQueue.queuePriority =.highlet lowPriorityQueue = OperationQueue()lowPriorityQueue.queuePriority =.lowfor i in 1...3 {    let highPriorityOperation = NSBlockOperation {        print("High priority task \(i) is running")    }    highPriorityQueue.addOperation(highPriorityOperation)    let lowPriorityOperation = NSBlockOperation {        print("Low priority task \(i) is running")    }    lowPriorityQueue.addOperation(lowPriorityOperation)}

在这种情况下,highPriorityQueue中的任务会优先于lowPriorityQueue中的任务开始执行,尽管它们添加到队列的时间可能相同。不过,需要注意的是,同一队列中任务的执行顺序还受到任务依赖关系和最大并发数等因素的影响。如果highPriorityQueue的最大并发数已满,lowPriorityQueue中的任务在满足其自身队列的执行条件时也会开始执行。

GCD(Grand Central Dispatch)拓展

  1. 调度队列在复杂场景中的应用
let dataProcessingQueue = DispatchQueue(label: "com.example.dataProcessingQueue")let calculationQueue = DispatchQueue(label: "com.example.calculationQueue", attributes:.concurrent)dataProcessingQueue.async {    let processedData = self.processData()    calculationQueue.async {        let result = self.performCalculation(with: processedData)        // 处理计算结果    }}
let highPriorityQueue = DispatchQueue(label: "com.example.highPriorityQueue", qos:.userInteractive)let lowPriorityQueue = DispatchQueue(label: "com.example.lowPriorityQueue", qos:.background)highPriorityQueue.async {    // 响应用户点击按钮操作    self.updateUI()}lowPriorityQueue.async {    // 后台更新数据库    self.updateDatabase()}
  • 多队列协作:在大型项目中,可能需要多个不同类型的队列协同工作。例如,一个数据处理模块可能有一个串行队列专门负责数据的顺序处理,避免数据竞争,同时有一个并行队列用于加速一些独立的计算任务。
  • 基于优先级的调度:在处理用户交互和后台任务时,合理设置队列优先级至关重要。例如,对于实时响应用户操作的任务,可以将其添加到优先级较高的队列中,而一些非关键的后台数据更新任务则可以放在优先级较低的队列。
  1. 任务提交的异常处理
let serialQueue = DispatchQueue(label: "com.example.serialQueue")do {    try serialQueue.sync {        throw NSError(domain: "com.example.error", code: 1, userInfo: nil)    }} catch {    print("Error in sync task: \(error)")}
typealias Result<T, E: Error> = Either<T, E>enum Either<L, R> {    case left(L)    case right(R)}let queue = DispatchQueue(label: "com.example.queue")queue.async {    let result: Result<String, Error>    do {        result =.right(try self.fetchData())    } catch {        result =.left(error)    }    switch result {    case.left(let error):        print("Error in async task: \(error)")    case.right(let data):        // 处理数据        print("Fetched data: \(data)")    }}
  • 同步任务异常:当同步任务中发生异常时,异常会直接抛出,影响当前线程的执行。例如:
  • 异步任务异常:异步任务中的异常处理相对复杂,因为任务在后台执行,不能直接通过try - catch捕获。一种常见的方式是在任务闭包中使用Result类型来传递结果和错误信息。
  1. 组队列与 Barrier 任务的优化策略
let group = DispatchGroup()let queue = DispatchQueue(label: "com.example.groupQueue")let workItems = (1...10).map { index in    DispatchWorkItem {        print("Task \(index) in group queue")    }}workItems.forEach { workItem in    group.enter()    queue.async(execute: workItem)    workItem.notify(queue:.main) {        group.leave()    }}group.notify(queue:.main) {    print("All tasks in group completed")}
  • 组队列优化:在使用DispatchGroup时,如果任务数量较多,可以考虑使用DispatchWorkItem来批量添加任务,提高效率。例如:
  • Barrier 任务优化:对于频繁使用 Barrier 任务的场景,可以缓存 Barrier 任务的执行结果,减少重复计算。例如,在一个多线程读写的数据库操作中,Barrier 任务用于更新数据库结构,在更新完成后,可以将新的数据库结构信息缓存起来,下次需要时直接读取缓存,避免再次执行 Barrier 任务。

OperationQueue 拓展

  1. 任务的生命周期管理
let operation = NSBlockOperation {    // 任务逻辑    for i in 1...10 {        if self.operation?.isCancelled == true {            return        }        print("Task progress: \(i)")    }}let queue = OperationQueue()queue.addOperation(operation)// 模拟在某个时刻取消任务DispatchQueue.main.asyncAfter(deadline:.now() + 2) {    operation.cancel()}
let operation = NSBlockOperation {    // 任务逻辑    print("Operation completed")}operation.completionBlock = {    print("Operation completion block executed")}let queue = OperationQueue()queue.addOperation(operation)
  • 任务取消:在NSOperation中,可以通过cancel()方法取消任务。例如:
  • 任务完成通知:除了通过依赖关系和队列完成通知外,还可以为NSOperation添加完成块。例如:
  1. 与其他框架的集成
let persistentContainer = NSPersistentContainer(name: "MyAppDataModel")persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in    if let error = error as NSError? {        fatalError("Unresolved error \(error), \(error.userInfo)")    }})let backgroundContext = persistentContainer.newBackgroundContext()let operationQueue = OperationQueue()let fetchOperation = NSBlockOperation {    backgroundContext.performAndWait {        let fetchRequest: NSFetchRequest<MyEntity> = MyEntity.fetchRequest()        do {            let results = try backgroundContext.fetch(fetchRequest)            // 处理查询结果        } catch {            print("Error fetching data: \(error)")        }    }}operationQueue.addOperation(fetchOperation)
let asset = AVAsset(url: videoURL)let assetReader = try! AVAssetReader(asset: asset)let videoOutput = AVAssetReaderTrackOutput(track: asset.tracks(withMediaType:.video).first!, outputSettings: nil)assetReader.add(videoOutput)let operationQueue = OperationQueue()let readOperation = NSBlockOperation {    if assetReader.startReading() {        while let sampleBuffer = videoOutput.copyNextSampleBuffer() {            // 处理视频帧        }    } else {        print("Error reading video: \(assetReader.error)")    }}operationQueue.addOperation(readOperation)
  • 与 Core Data 集成:在使用 Core Data 进行数据持久化时,OperationQueue可以用于管理数据的读写操作,避免多线程访问导致的数据冲突。例如:
  • 与 AVFoundation 集成:在处理音频、视频等多媒体任务时,OperationQueue可以用于管理不同的操作,如音频解码、视频渲染等。例如:
  1. 自定义队列的性能优化
class OperationQueueManager {    static let shared = OperationQueueManager()    private let generalQueue = OperationQueue()    private let highPriorityQueue = OperationQueue()    private init() {        highPriorityQueue.queuePriority =.high    }    func getGeneralQueue() -> OperationQueue {        return generalQueue    }    func getHighPriorityQueue() -> OperationQueue {        return highPriorityQueue    }}
  • 队列复用:在应用中频繁创建和销毁OperationQueue会消耗资源,因此可以考虑复用队列。例如,创建一个单例的OperationQueue管理类,提供不同用途的队列实例。
  • 减少不必要的操作:在向OperationQueue添加任务时,应尽量确保任务的必要性。例如,在进行网络请求时,先检查缓存中是否有可用数据,如果有则避免重复添加网络请求任务。同时,合理设置任务的依赖关系,避免产生不必要的等待时间,提高队列的整体执行效率。

四、Swift 异步并发新特性

  1. async/await
  • 函数声明为异步的语法
  • await 表达式的使用
  • 处理异步函数的错误
  1. Task
  • 创建和启动 Task
  • Task 的生命周期管理
  • Task 的优先级与取消操作
  1. Actor
  • Actor 的定义与作用
  • 访问控制与线程安全
  • Actor 之间的通信

async/await

  1. 函数声明为异步的语法

在 Swift 中,通过在函数声明前添加async关键字,将函数标记为异步函数。异步函数可以暂停执行,等待其他异步操作完成,而不会阻塞当前线程。例如:

async func fetchUserData() async throws -> UserData {    // 模拟网络请求    try await Task.sleep(nanoseconds: 2_000_000_000)    return UserData(name: "John Doe", age: 30)}

上述代码定义了一个异步函数fetchUserData,它模拟了一个耗时 2 秒的网络请求,并返回一个UserData对象。async关键字表明该函数是异步的,throws关键字表示函数可能会抛出错误。

  1. await 表达式的使用

await表达式用于暂停异步函数的执行,直到其等待的异步操作完成。它只能在异步函数内部使用。例如:

async func processUserData() async {    do {        let userData = await fetchUserData()        print("Fetched user data: \(userData.name), \(userData.age)")    } catch {        print("Error fetching user data: \(error)")    }}

在processUserData函数中,通过await表达式等待fetchUserData函数的执行结果。当fetchUserData完成后,继续执行后续代码。

  1. 处理异步函数的错误

异步函数可以像普通函数一样使用try - catch块来处理错误。如果异步函数抛出错误,await表达式会将错误传递给调用者。例如:

async func fetchUserDataWithError() async throws -> UserData {    // 模拟网络请求失败    throw NSError(domain: "NetworkError", code: 404, userInfo: nil)}async func handleError() async {    do {        let userData = await fetchUserDataWithError()    } catch {        print("Error: \(error)")    }}

在这个例子中,fetchUserDataWithError函数模拟了一个网络请求失败的情况,抛出了一个NSError。handleError函数通过try - catch块捕获并处理这个错误。

Task

  1. 创建和启动 Task

Task是 Swift 中用于表示异步任务的类型。可以通过Task { }闭包来创建一个新的任务,并自动启动它。例如:

let task = Task {    let result = await someAsyncFunction()    print("Task result: \(result)")}

上述代码创建了一个新的Task,在任务闭包中调用了一个异步函数someAsyncFunction,并打印其结果。任务会立即开始执行。

  1. Task 的生命周期管理

Task有自己的生命周期,包括创建、运行、暂停、恢复和取消。可以通过Task.current获取当前任务的引用,并对其进行管理。例如,通过Task.cancel()方法可以取消一个任务:

let task = Task {    for i in 1...10 {        if Task.isCancelled {            break        }        print("Task progress: \(i)")        try await Task.sleep(nanoseconds: 1_000_000_000)    }}// 模拟在某个时刻取消任务DispatchQueue.main.asyncAfter(deadline:.now() + 3) {    task.cancel()}

在这个例子中,任务会在循环中打印进度,每 1 秒打印一次。如果在 3 秒后任务被取消,循环会提前结束。

  1. Task 的优先级与取消操作

Task可以设置优先级,通过TaskPriority枚举来指定。例如:

let highPriorityTask = Task(priority:.high) {    // 高优先级任务逻辑}let lowPriorityTask = Task(priority:.low) {    // 低优先级任务逻辑}

高优先级的任务会在资源竞争时优先获得执行机会。同时,如前面示例所示,通过cancel()方法可以取消任务,任务内部可以通过Task.isCancelled检查是否被取消,从而做出相应的处理。

Actor

  1. Actor 的定义与作用

Actor是一种用于隔离状态和同步访问的机制,确保同一时间只有一个任务可以访问其可变状态。它可以有效地避免数据竞争和线程安全问题。例如,假设有一个需要共享可变状态的场景,如一个计数器:

actor Counter {    private var value = 0    func increment() {        value += 1    }    func getValue() -> Int {        return value    }}

在这个Counter actor 中,value属性是私有的,只能通过increment和getValue方法访问。由于 actor 的特性,这些方法在执行时是线程安全的,不会出现数据竞争问题。

  1. 访问控制与线程安全

当从外部访问 actor 的方法时,需要使用await表达式,因为对 actor 的访问是异步的。例如:

let counter = Counter()Task {    await counter.increment()    let currentValue = await counter.getValue()    print("Current value of counter: \(currentValue)")}

在上述代码中,通过await调用counter的increment和getValue方法,确保了对counter状态的访问是线程安全的,即使在多任务并发环境下也不会出现问题。

  1. Actor 之间的通信

Actor 之间可以通过异步函数调用来进行通信。例如,假设有两个 actor ActorA和ActorB,ActorA需要向ActorB发送消息:

actor ActorA {    let actorB: ActorB    init(actorB: ActorB) {        self.actorB = actorB    }    func sendMessage() {        Task {            await actorB.receiveMessage(message: "Hello from ActorA")        }    }}actor ActorB {    func receiveMessage(message: String) {        print("Received message: \(message)")    }}let actorB = ActorB()let actorA = ActorA(actorB: actorB)actorA.sendMessage()

在这个例子中,ActorA通过创建一个新的Task来异步调用ActorB的receiveMessage方法,实现了 actor 之间的通信,同时保证了线程安全。

五、异步并发的实践应用

  1. 网络请求
  • 使用异步函数进行 HTTP 请求
  • 处理请求中的数据解析与错误
  • 多请求并发与顺序执行
  1. 数据存储
  • 异步写入与读取本地存储
  • 数据库操作中的并发控制
  1. UI 更新
  • 确保 UI 更新在主线程执行
  • 异步任务完成后安全更新 UI

网络请求

  1. 使用异步函数进行 HTTP 请求

在 Swift 中,结合async/await与URLSession可以简洁高效地进行 HTTP 请求。例如,进行一个 GET 请求获取 JSON 数据:

func fetchJSONData(from url: URL) async throws -> Data {    let (data, response) = try await URLSession.shared.data(from: url)    guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {        throw NSError(domain: "NetworkError", code: (response as? HTTPURLResponse)?.statusCode?? -1, userInfo: nil)    }    return data}

上述代码定义了一个异步函数fetchJSONData,它接收一个 URL,使用URLSession的data(from:)方法发起请求。await关键字使函数等待请求完成,获取到数据和响应。接着检查响应状态码,若状态码不在 200 - 299 范围内,则抛出错误。

  1. 处理请求中的数据解析与错误

在获取到数据后,通常需要进行解析。以解析 JSON 数据为例:

struct User: Codable {    let name: String    let age: Int}async func fetchAndParseUser(from url: URL) async throws -> User {    let data = try await fetchJSONData(from: url)    do {        let decoder = JSONDecoder()        return try decoder.decode(User.self, from: data)    } catch {        throw NSError(domain: "JSONDecodeError", code: -1, userInfo: nil)    }}

此函数先调用fetchJSONData获取数据,然后使用JSONDecoder将数据解析为User结构体。若解析过程出现错误,捕获并抛出一个自定义的NSError。

  1. 多请求并发与顺序执行

有时需要同时发起多个网络请求以提高效率,可利用Task实现并发请求。例如,同时获取用户信息和用户的订单列表:

func fetchUserOrders(from url: URL) async throws -> [Order] {    // 模拟订单数据解析逻辑    let data = try await fetchJSONData(from: url)    let decoder = JSONDecoder()    return try decoder.decode([Order].self, from: data)}struct Order: Codable {    let orderId: String    let amount: Double}async func concurrentRequests() async {    let userURL = URL(string: "https://example.com/api/user")!    let ordersURL = URL(string: "https://example.com/api/orders")!    let userTask = Task { try await fetchAndParseUser(from: userURL) }    let ordersTask = Task { try await fetchUserOrders(from: ordersURL) }    do {        let user = try await userTask.value        let orders = try await ordersTask.value        print("User: \(user.name), Orders: \(orders.count)")    } catch {        print("Error in concurrent requests: \(error)")    }}

上述代码中,userTask和ordersTask分别执行用户信息和订单列表的获取任务,它们并发执行。通过await获取每个任务的结果,处理成功或失败情况。

若请求存在依赖关系,需顺序执行。比如先获取用户令牌,再用令牌获取用户详细信息:

async func fetchToken() async throws -> String {    // 模拟获取令牌逻辑    let data = try await fetchJSONData(from: URL(string: "https://example.com/api/token")!)    let decoder = JSONDecoder()    let tokenResponse = try decoder.decode(TokenResponse.self, from: data)    return tokenResponse.token}struct TokenResponse: Codable {    let token: String}async func fetchUserDetails(with token: String) async throws -> UserDetails {    var request = URLRequest(url: URL(string: "https://example.com/api/user/details")!)    request.httpMethod = "GET"    request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")    let data = try await fetchJSONData(from: request.url!)    let decoder = JSONDecoder()    return try decoder.decode(UserDetails.self, from: data)}struct UserDetails: Codable {    let email: String    let address: String}async func sequentialRequests() async {    do {        let token = try await fetchToken()        let userDetails = try await fetchUserDetails(with: token)        print("User details: \(userDetails.email), \(userDetails.address)")    } catch {        print("Error in sequential requests: \(error)")    }}

在此例中,fetchToken先执行获取令牌,然后fetchUserDetails使用获取到的令牌发起请求,顺序执行两个请求。

数据存储

  1. 异步写入与读取本地存储

使用UserDefaults进行简单数据存储时,可将写入操作异步化,避免阻塞主线程。例如,保存用户设置:

func saveUserSettingsAsync(settings: [String: Any]) async {    await withCheckedContinuation { continuation in        DispatchQueue.global().async {            UserDefaults.standard.set(settings, forKey: "UserSettings")            continuation.resume()        }    }}

上述代码通过DispatchQueue.global()在全局队列中执行写入UserDefaults的操作,使用withCheckedContinuation将异步操作包装成async函数。

读取本地存储也可类似处理,如读取用户设置:

func fetchUserSettingsAsync() async -> [String: Any]? {    return await withCheckedContinuation { continuation in        DispatchQueue.global().async {            let settings = UserDefaults.standard.dictionary(forKey: "UserSettings")            continuation.resume(returning: settings)        }    }}

这里在全局队列中读取UserDefaults数据,通过continuation.resume(returning:)返回结果。

对于更复杂的文件存储,如存储图片,可利用FileManager结合async/await:

func saveImageToFile(image: UIImage, atPath path: String) async throws {    if let data = image.jpegData(compressionQuality: 0.8) {        try await withCheckedThrowingContinuation { continuation in            DispatchQueue.global().async {                do {                    try data.write(to: URL(fileURLWithPath: path))                    continuation.resume()                } catch {                    continuation.resume(throwing: error)                }            }        }    } else {        throw NSError(domain: "ImageDataError", code: -1, userInfo: nil)    }}

此函数将UIImage转换为 JPEG 数据,在全局队列中异步写入指定路径的文件。

  1. 数据库操作中的并发控制

在使用 Core Data 进行数据库操作时,NSManagedObjectContext的并发访问需要谨慎处理。例如,在多线程环境下保存数据:

let persistentContainer = NSPersistentContainer(name: "MyAppDataModel")persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in    if let error = error as NSError? {        fatalError("Unresolved error \(error), \(error.userInfo)")    }})func saveDataToCoreDataAsync(data: MyData) async throws {    let backgroundContext = persistentContainer.newBackgroundContext()    try await backgroundContext.perform {        let entity = NSEntityDescription.entity(forEntityName: "MyDataEntity", in: backgroundContext)!        let managedObject = NSManagedObject(entity: entity, insertInto: backgroundContext)        managedObject.setValue(data.value1, forKey: "attribute1")        managedObject.setValue(data.value2, forKey: "attribute2")        do {            try backgroundContext.save()        } catch {            throw error        }    }}struct MyData {    let value1: String    let value2: Int}

这里创建一个新的后台NSManagedObjectContext,使用perform方法在其关联的队列中执行数据插入和保存操作,确保线程安全。

UI 更新

  1. 确保 UI 更新在主线程执行

在 Swift 中,所有 UI 更新操作必须在主线程执行。利用async/await和Task可以方便地将异步任务结果用于 UI 更新。例如,从网络获取图片后更新 UIImageView:

func fetchImage(from url: URL) async throws -> UIImage? {    let data = try await fetchJSONData(from: url)    return UIImage(data: data)}async func updateImageView(imageView: UIImageView, with url: URL) async {    do {        let image = try await fetchImage(from: url)        await MainActor.run {            imageView.image = image        }    } catch {        print("Error fetching image: \(error)")    }}

fetchImage函数异步获取图片数据并转换为UIImage。updateImageView函数获取图片后,通过MainActor.run在主线程更新UIImageView的图片。

  1. 异步任务完成后安全更新 UI

当有多个异步任务同时进行,且完成后都需要更新 UI 时,要确保 UI 更新的安全性和正确性。例如,同时加载多个图片并显示在不同的UIImageView中:

let imageViews = [imageView1, imageView2, imageView3]let urls = [URL(string: "https://image1.com")!, URL(string: "https://image2.com")!, URL(string: "https://image3.com")!]let tasks = zip(imageViews, urls).map { imageView, url in    Task {        do {            let image = try await fetchImage(from: url)            await MainActor.run {                imageView.image = image            }        } catch {            print("Error fetching image for \(url): \(error)")        }    }}await withTaskGroup(of: Void.self) { group in    tasks.forEach { task in        group.addTask { await task.value }    }    for await _ in group {        // 等待所有任务完成    }}

上述代码创建多个任务,每个任务负责为一个UIImageView加载并设置图片。通过withTaskGroup等待所有任务完成,确保所有图片加载和 UI 更新操作安全执行。

六、性能优化与调试

  1. 性能分析工具
  • 使用 Instruments 分析异步性能
  • 查找性能瓶颈与优化点
  1. 死锁与资源竞争
  • 死锁的产生原因与预防
  • 资源竞争的检测与解决
  1. 代码优化技巧
  • 减少不必要的异步操作
  • 合理设置并发数与任务优先级

性能分析工具

  1. 使用 Instruments 分析异步性能
  • 配置与启动:Instruments 是 Xcode 内置的强大性能分析工具。在 Xcode 中,选择 Product > Profile 来启动 Instruments。在模板选择界面,针对异步并发应用,通常选择 “Time Profiler” 模板用于分析函数执行时间,或 “Leaks” 模板用于检测内存泄漏(在异步任务频繁创建和销毁对象时可能出现)。例如,在一个包含大量异步网络请求和数据处理的应用中,使用 “Time Profiler” 模板可以清晰地看到每个异步函数的执行时长。
  • 解读时间轴:启动分析后,Instruments 会生成一个时间轴,展示应用在运行过程中的各种活动。在时间轴中,不同颜色的线条代表不同类型的活动,如 CPU 使用、磁盘 I/O、网络请求等。对于异步函数,可通过时间轴观察其何时开始执行、执行持续时间以及是否存在频繁的上下文切换。例如,如果一个异步数据处理函数执行时间过长,且在时间轴上显示该函数执行期间 CPU 使用率过高,可能需要优化该函数的算法或检查是否存在不必要的资源占用。
  • 函数调用树分析:Instruments 的函数调用树视图能展示函数之间的调用关系以及每个函数的执行时间占比。在异步并发场景中,这有助于找出性能瓶颈所在的函数。例如,在一个复杂的异步任务链中,通过调用树发现某个中间转换函数占用了大量执行时间,进一步分析该函数的代码逻辑,可能发现存在多次重复计算或低效的数据结构操作,从而针对性地进行优化。
  1. 利用 Swift 的@_optimize(none)进行调试
  • 临时禁用优化:在调试异步代码时,Swift 的优化机制有时会干扰对代码执行过程的理解。通过在函数或代码块前添加@_optimize(none)属性,可以临时禁用 Swift 编译器的优化。例如,在一个复杂的异步递归函数中,由于优化可能导致函数执行顺序与预期不符,难以跟踪调试。此时在该函数定义前添加@_optimize(none),编译器会按照代码的书写顺序执行,便于设置断点、观察变量值变化,从而找出逻辑错误。
  • 对比优化前后效果:在优化代码前和优化后,分别使用@_optimize(none)运行代码,对比性能和执行结果。在优化前,代码可能执行较慢但逻辑清晰易于调试;优化后,性能提升但可能引入新的问题。通过对比,可以确保优化过程没有破坏原有功能,并且真正提升了性能。例如,在对一个异步数据排序函数进行优化后,使用@_optimize(none)对比优化前后的排序结果是否一致,以及在不同数据规模下的执行时间差异。

死锁与资源竞争

  1. 死锁的产生原因与预防
let queue1 = DispatchQueue(label: "queue1")let queue2 = DispatchQueue(label: "queue2")queue1.sync {    print("Task in queue1")    queue2.sync {        print("Task in queue2")    }}

上述代码中,queue1中的同步任务等待queue2中的同步任务完成,而queue2中的同步任务又在等待queue1中的任务完成,从而导致死锁。

let queue1 = DispatchQueue(label: "queue1")let queue2 = DispatchQueue(label: "queue2")queue1.async {    print("Task in queue1")    queue2.async {        print("Task in queue2")    }}

此外,设置合理的超时机制也是预防死锁的有效方法。在等待资源时,如果超过一定时间仍未获取到资源,则放弃等待并进行相应处理,避免无限期等待。

  • 常见原因:死锁通常发生在多个任务相互等待对方释放资源的情况下。在异步并发环境中,例如,任务 A 持有资源 1 并等待资源 2,而任务 B 持有资源 2 并等待资源 1,就会形成死锁。在使用DispatchQueue时,如果错误地设置同步任务的依赖关系,也可能导致死锁。例如:
  • 预防策略:为预防死锁,应遵循资源获取的顺序一致性原则。例如,在所有任务中,按照相同的顺序获取资源。对于DispatchQueue,避免在同步任务中嵌套同步调用其他队列。可以使用异步调用代替,如将上述代码修改为:
  1. 资源竞争的检测与解决
let lock = NSLock()func accessSharedResource() {    lock.lock()    // 访问和修改共享资源的代码    lock.unlock()}

另一种方法是使用Actor,Actor会自动确保同一时间只有一个任务可以访问其可变状态,从根本上避免资源竞争。例如,将共享资源封装在一个Actor中:

actor SharedData {    private var value = 0    func updateValue() {        value += 1    }    func getValue() -> Int {        return value    }}

这样,不同任务对SharedData中value的访问和修改就不会产生资源竞争。

  • 检测方法:资源竞争发生在多个任务同时访问和修改共享资源时。使用 Instruments 的 “Thread Sanitizer” 模板可以检测资源竞争。在应用运行时,“Thread Sanitizer” 会监控所有线程对共享资源的访问,一旦发现竞争情况,会在控制台输出详细的错误信息,包括竞争发生的位置、涉及的线程等。例如,在一个多线程异步操作共享数据库的场景中,通过 “Thread Sanitizer” 检测到两个线程同时尝试写入同一数据行,从而发现资源竞争问题。
  • 解决策略:解决资源竞争的常用方法是使用锁机制。在 Swift 中,可以使用NSLock、NSRecursiveLock或DispatchSemaphore来保护共享资源。例如,使用NSLock对共享资源进行访问控制:

代码优化技巧

  1. 减少不必要的异步操作
async func fetchCombinedData() async throws -> CombinedData {    let (data1, data2) = try await withTaskGroup(of: (Data, Data).self) { group in        group.addTask { try await fetchDataFromAPI1() }        group.addTask { try await fetchDataFromAPI2() }        var result1: Data?        var result2: Data?        for try await data in group {            if result1 == nil {                result1 = data            } else {                result2 = data            }        }        return (result1!, result2!)    }    // 处理合并后的数据    return processCombinedData(data1, data2)}
  • 合并异步任务:在一些情况下,多个小的异步任务可以合并为一个大的异步任务,减少任务调度开销。例如,在一个需要从多个 API 获取数据并进行组合处理的场景中,如果每个 API 请求都作为一个独立的异步任务,会增加系统的负担。可以将这些请求合并为一个异步函数,在函数内部按顺序或并发执行这些请求。例如:
  • 避免过度异步化:并非所有操作都需要异步执行。对于一些执行时间极短的操作,异步化反而会增加开销。例如,简单的本地数据计算操作,如果将其放在异步任务中执行,任务的创建、调度和切换上下文的时间可能比计算本身的时间还长。因此,在决定是否将操作异步化时,要综合考虑操作的性质和执行时间。
  1. 合理设置并发数与任务优先级
let maxConcurrentCount = ProcessInfo.processInfo.processorCount * 2let operationQueue = OperationQueue()operationQueue.maxConcurrentOperationCount = maxConcurrentCount
let highPriorityOperation = NSBlockOperation {    // 处理用户交互任务}highPriorityOperation.queuePriority =.highlet lowPriorityOperation = NSBlockOperation {    // 处理后台同步任务}lowPriorityOperation.queuePriority =.low
  • 根据设备性能调整并发数:不同设备的性能不同,应根据设备的 CPU 核心数、内存大小等因素合理设置并发数。在 iOS 设备上,可以通过ProcessInfo.processInfo.processorCount获取 CPU 核心数。例如,在一个处理大量图片的应用中,如果并发数设置过高,可能导致设备内存不足或 CPU 负载过大,影响应用性能。可以根据设备 CPU 核心数设置一个合理的并发数,如:
  • 基于任务重要性设置优先级:对于用户交互相关的任务,如响应按钮点击、实时更新 UI 等,应设置较高的优先级;而对于一些后台数据同步、日志记录等非关键任务,设置较低的优先级。例如,在一个社交应用中,用户发送消息的任务优先级应高于后台同步好友列表的任务。通过设置任务优先级,可以确保重要任务及时得到执行,提升用户体验。在OperationQueue中,可以通过queuePriority属性设置任务优先级:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值