Swift Concurrency 并发编程,异步任务与Task构造体

所属分类:ios | 发布于 2024-02-13 09:18:13

什么事Swift Concurrency?

Swift Concurrency是Swift5.5新增的功能之一,提供了更方便的异步变成(并行处理)的功能。Swift Concurrency提供了asyncawait关键字、Task结构等,可以消除在异步处理时使用回调函数completionHandler带来的循环嵌套导致可读性降低的问题。

回调函数

在并发编程出现之前,一般使用回调函数处理异步任务

func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    sleep(2)
    let data = Data() // 从服务器获取数据(成功 or 失敗)
    completion(.success(data))
}

print("START")

fetchData { result in
    switch result {
    case .success(let data):
        print("Data received: \(data)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

async/await初探

func fetchData() async throws -> Data {
    sleep(2)
    let data = Data()  // 从服务器获取数据(成功 or 失敗)
    return data
}

print("START")

Task {
    do {
        let data = try await fetchData()
        print("Data received: \(data)")
    } catch {
        print("Error: \(error)")
    }
}

async关键字

async关键字来自异步“Asynchronous”的前5个字母。带有async关键字的函数(方法)被视为异步函数(方法)。这表明这个函数是一种特殊类型的函数,它有异步执行和执行过程中中断的可能性

声明一个异步函数

func fetchData() async throws -> Data { }

如果函数会抛出错误要附加throws关键字时,必须按async throws的顺序进行定义。

await关键字

await关键字用于标记正在调用异步函数(方法),代码运行到该标记,将执行暂停(即串联),直到调用的异步函数完成(返回)。

如下面的代码,输出1后,会在fetchData完成再输出2

print("1")
let data = await fetchData() // 2秒かかる処理
print("2")

如果不带await调用带有async的函数,则会出现以下错误

Expression is 'async' but is not marked with 'await'

另外,如果从其它函数调用带有async的异步函数,则该函数也必须被标记为async

func parentFunction() async {
  print("1")
  let data = await fetchData() // 2秒处理
  print("2") // fetchData完成2秒后调用
}

如果不标记,则会出现一下错误

'async' call in a function that does not support concurrency

Task结构体

Task结构是用于管理异步函数(方法)的结构。这里所说的异步函数是指赋予async关键字的函数,使用await关键字调用。

在iOS中,任务是异步工作的单位,异步处理在一个任务中进行

标记了async关键字的函数,如果直接调用,则会出现错误,因此要执行async函数,可以使用Task结构体包裹起来。Task.init的定义接受一个闭包,该闭包带有async。

init(priority: TaskPriority?, operation: () async -> Success)

利用Swift的Trailing Closure尾随闭包特性,Task的初始化可以简化为Task{ }

Task {
    print("1")
    let data = await fetchData() // 该函数需要2秒时间处理
    print("2")
}

创建任务实例后,将立即执行传递给存储模块的操作,并允许您使用此实例执行任务操作。

另外,还可以在Task中执行带有throws关键字的函数,而不需要编写do-catch语句。但是请记住,在Task中,即使发生错误页不会发生任何事情,所以如果需要错误处理的话,则可以在里面使用do-catch语句。

Task {
    let data = try await fetchData()
}

 

并行处理

因为任务是异步处理,所以不是串联的,而是并行的。

func fetchData() -> Data {
    Task {
        try await Task.sleep(nanoseconds: 1000000000) // 1秒停止
        print("非同期処理1")
    }
    
    Task {
        try await Task.sleep(nanoseconds: 2000000000) // 2秒停止
        print("非同期処理2")
    }
    
    Task {
        try await Task.sleep(nanoseconds: 3000000000) // 3秒停止print("非同期処理1")
        print("非同期処理3")
    }
    return Data()
}

print("START")

let data = fetchData()

print("END")

上面的代码执行结果如下

START     // 运行开始后立即输出
END       // 运行开始后立即输出
非同期処理1 // 运行开始1秒后
非同期処理2 // 运行开始2秒后
非同期処理3 // 运行开始3秒后

代码运行3秒后全部输出完成,而不用等到1+2+3=6秒后才全部输出完成,因为这3个Task是并行执行的。

任务操作

获取任务实例后,可以执行各种操作

任务暂停

可以使用Task.sleep(nanoseconds:)方法停止任务处理。因为这是static,所以即使不实例化也可以调用。sleep方法也是async throws,所以需要try await。

func fetchData() async throws -> Data {
    try await Task.sleep(nanoseconds: 2000000000) // 2秒停止
    let data = Data()  // 从服务器获取数据(成功 or 失敗)
    return data
}

因为参数需要用纳秒传递,所以2秒的话会变成20000000000。

取消任务

要取消任务,请执行cancel方法。执行时,异步函数不再运行。取消任务时抛出CancellationError错误。例如,执行以下操作,则输出1后输出Error:CancellationError()。

let myTask = Task {
    do {
        print("1")
        let data = try await fetchData()
        print("2")
    } catch {
        print("Error: \(error)") // Error: CancellationError()
    }
}

myTask.cancel()

避开主线程

任务始终在主线程运行,因为它具有@MainiActor。但是,只保存在没有UI更新的文件中的处理,如果主线程执行的话,UI的相应只会变差,所以为了避免这种情况,准备了detached方法。现在,Task.detached中的处理将在后台线程中执行。

Task {
  // 在主线程上运行
}

Task.detached {
  // 在后台线程上运行
}

但是测试后发现,添加了async关键字的函数基本上都是在后台线程中执行

确认了一下线程,确实async内是后台线程,Task内是主线程。

func fetchData() async throws -> Data {
    print(Thread.current) // <NSThread: 0x600001737700>{number = 6, name = (null)}
    sleep(2)
    let data = Data()  // 从服务器获取数据(成功 or 失敗)
    return data
}

Task {
    print(Thread.current) // <_NSMainThread: 0x600001700140>{number = 1, name = main}
    let data = try await fetchData()
    print(Thread.current) // <_NSMainThread: 0x600001700140>{number = 1, name = main}
}

Task.detached中更改为后台线程的部分好像是异步函数及其作为一系列处理包围的部分。

Task.detached {
    print(Thread.current) // <NSThread: 0x600001737700>{number = 6, name = (null)}
    let data = try await fetchData()
    print(Thread.current) // <NSThread: 0x600001737700>{number = 6, name = (null)}
}

 

async let

在并行处理时,通过使用await可以暂停到该处理完成为止,但是也有想进一步并行处理不同的异步函数的情况。例如,如果有三个过程下载不同的头像,那么await将在第一个图像下载完成后开始第二个下载。但是实际上我们希望同时下载3个,所以可以用Task将下载过程一个一个的围起来,但是使用async let可以轻松的标记并行处理。

Task {
  let firstPhoto = await downloadPhoto(named: photoNames[0])
  let secondPhoto = await downloadPhoto(named: photoNames[1])
  let thirdPhoto = await downloadPhoto(named: photoNames[2])
  let photos = [firstPhoto, secondPhoto, thirdPhoto]
  show(photos)
}

通过在async let中声明变量,不等待该函数的处理,而是继续执行下面操作。可以在想要取得结果的地方使用await获取结果。

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

 

 

文哥博客(https://wenge365.com)属于文野个人博客,欢迎浏览使用

联系方式:qq:52292959 邮箱:52292959@qq.com

备案号:粤ICP备18108585号 友情链接