MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift并发模型中的async/await实战

2024-11-194.3k 阅读

Swift 并发模型简介

在现代软件开发中,并发编程是提高应用程序性能和响应性的关键技术。Swift 作为一种现代编程语言,提供了强大且易于使用的并发模型,特别是通过 async/await 语法,使得异步编程更加直观和安全。

Swift 的并发模型基于结构化并发的概念。结构化并发意味着任务之间存在明确的层次关系,当一个父任务完成时,其所有子任务也会自动取消。这种结构有助于管理资源和避免内存泄漏,同时使得代码的逻辑更加清晰。

async/await 基础

async 关键字用于标记一个异步函数,这意味着该函数不会阻塞调用线程,而是在后台执行。例如:

func fetchData() async -> Data? {
    // 模拟网络请求等异步操作
    await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    return Data("Sample data".utf8)
}

在上述代码中,fetchData 函数被标记为 async,表明它是一个异步函数。await Task.sleep 模拟了一个耗时操作,比如网络请求。

await 关键字用于暂停当前函数的执行,直到异步任务完成。它只能在 async 函数内部使用。例如:

func processData() async {
    if let data = await fetchData() {
        // 处理数据
        print(String(data: data, encoding:.utf8) ?? "No data")
    }
}

processData 函数中,await fetchData() 会暂停函数执行,直到 fetchData 完成并返回数据。

并发任务的创建与管理

Task 的创建

使用 Task 结构体可以创建新的并发任务。例如:

let task = Task {
    let data = await fetchData()
    if let data = data {
        // 处理数据
        print(String(data: data, encoding:.utf8) ?? "No data")
    }
}

这里创建了一个新的 Task,其闭包内的代码会在后台异步执行。

任务的取消

由于结构化并发的特性,当父任务取消时,子任务也会自动取消。也可以手动取消任务,例如:

let cancellableTask = Task {
    for i in 1...10 {
        if Task.isCancelled {
            return
        }
        print("Task is running: \(i)")
        await Task.sleep(nanoseconds: 1_000_000_000)
    }
}
// 稍后取消任务
cancellableTask.cancel()

在上述代码中,Task.isCancelled 用于检查任务是否被取消,如果被取消,则提前返回,避免不必要的计算。

处理多个异步任务

并发执行多个任务

可以使用 Task.group 来并发执行多个异步任务。例如:

func fetchMultipleData() async {
    let task1 = Task { () -> Data? in
        await Task.sleep(nanoseconds: 2 * 1_000_000_000)
        return Data("Data from task 1".utf8)
    }
    let task2 = Task { () -> Data? in
        await Task.sleep(nanoseconds: 3 * 1_000_000_000)
        return Data("Data from task 2".utf8)
    }
    let results = await [task1.value, task2.value]
    for result in results where result!= nil {
        print(String(data: result!, encoding:.utf8) ?? "No data")
    }
}

fetchMultipleData 函数中,task1task2 并发执行,await [task1.value, task2.value] 会等待两个任务都完成,并返回结果数组。

按顺序执行多个任务

有时候需要按顺序执行多个异步任务,可以简单地在 async 函数中依次调用异步函数。例如:

func processDataSequentially() async {
    let data1 = await fetchData()
    if let data1 = data1 {
        // 处理 data1
        let processedData1 = processData1(data1)
        let data2 = await fetchAnotherData(processedData1)
        if let data2 = data2 {
            // 处理 data2
            processData2(data2)
        }
    }
}

在这个例子中,fetchAnotherData 会等待 fetchData 完成并处理完 data1 后才会执行。

错误处理

异步函数中的错误抛出

异步函数可以像普通函数一样抛出错误。例如:

func fetchDataWithError() async throws -> Data {
    // 模拟网络请求失败
    if arc4random_uniform(2) == 0 {
        throw NetworkError.requestFailed
    }
    await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    return Data("Sample data".utf8)
}
enum NetworkError: Error {
    case requestFailed
}

fetchDataWithError 函数中,通过 throw NetworkError.requestFailed 抛出错误。

捕获异步函数中的错误

在调用可能抛出错误的异步函数时,需要使用 try await 来捕获错误。例如:

func handleDataWithError() async {
    do {
        let data = try await fetchDataWithError()
        print(String(data: data, encoding:.utf8) ?? "No data")
    } catch {
        print("Error: \(error)")
    }
}

handleDataWithError 函数中,try await 捕获 fetchDataWithError 抛出的错误,并在 catch 块中处理。

与其他异步编程模型的结合

与 GCD 的结合

虽然 Swift 有自己的并发模型,但有时候也需要与 Grand Central Dispatch (GCD) 结合使用。例如,可以将 GCD 的 DispatchQueueasync/await 结合:

func performOnDispatchQueue() async {
    await withCheckedContinuation { continuation in
        DispatchQueue.global().async {
            // 异步操作
            let result = performSomeCalculation()
            continuation.resume(returning: result)
        }
    }
}
func performSomeCalculation() -> Int {
    // 模拟计算
    return 42
}

在上述代码中,withCheckedContinuation 用于将 GCD 异步操作包装成 async/await 形式。

与 Combine 的结合

Combine 是 Swift 中的响应式编程框架,也可以与 async/await 结合。例如,将 Future(Combine 中的异步类型)转换为 async/await 形式:

import Combine
func fetchDataWithCombine() -> Future<Data, Error> {
    Future { promise in
        // 模拟网络请求
        let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
            if arc4random_uniform(2) == 0 {
                promise(.failure(NetworkError.requestFailed))
            } else {
                promise(.success(Data("Sample data".utf8)))
            }
        }
        RunLoop.current.add(timer, forMode:.common)
    }
}
func convertCombineToAsync() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        let cancellable = fetchDataWithCombine()
          .sink(receiveCompletion: { completion in
                switch completion {
                case.success: break
                case.failure(let error):
                    continuation.resume(throwing: error)
                }
            }, receiveValue: { data in
                continuation.resume(returning: data)
            })
        continuation.onCancel = {
            cancellable.cancel()
        }
    }
}

在这个例子中,withCheckedThrowingContinuationFuture 转换为可以使用 async/await 的形式。

实战案例:网络请求与数据处理

网络请求封装

假设我们有一个简单的网络请求函数,使用 URLSession 进行异步请求,并将其封装为 async/await 形式:

func sendRequest(url: URL) async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data, error == nil else {
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(throwing: NetworkError.unknown)
                }
                return
            }
            continuation.resume(returning: data)
        }
        task.resume()
        continuation.onCancel = {
            task.cancel()
        }
    }
}
enum NetworkError: Error {
    case unknown
}

sendRequest 函数中,withCheckedThrowingContinuationURLSession 的异步任务转换为 async/await 可调用的形式。

数据解析与处理

假设我们从网络请求获取的数据是 JSON 格式,需要进行解析。例如:

struct User: Codable {
    let name: String
    let age: Int
}
func processNetworkData() async {
    let url = URL(string: "https://example.com/api/user")!
    do {
        let data = try await sendRequest(url: url)
        let decoder = JSONDecoder()
        let user = try decoder.decode(User.self, from: data)
        print("User: \(user.name), Age: \(user.age)")
    } catch {
        print("Error: \(error)")
    }
}

processNetworkData 函数中,首先通过 sendRequest 获取数据,然后使用 JSONDecoder 解析 JSON 数据为 User 结构体实例。

性能优化与注意事项

避免不必要的等待

在编写 async/await 代码时,要注意避免不必要的等待。例如,如果有多个异步任务相互独立,可以并发执行而不是顺序执行。

// 不好的做法,顺序执行任务
func processTasksSequentially() async {
    let result1 = await task1()
    let result2 = await task2()
    // 处理结果
}
// 好的做法,并发执行任务
func processTasksConcurrently() async {
    let task1Result = Task { await task1() }
    let task2Result = Task { await task2() }
    let result1 = await task1Result.value
    let result2 = await task2Result.value
    // 处理结果
}
func task1() async -> Int {
    await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    return 42
}
func task2() async -> Int {
    await Task.sleep(nanoseconds: 3 * 1_000_000_000)
    return 13
}

processTasksConcurrently 函数中,task1task2 并发执行,提高了整体执行效率。

资源管理

由于结构化并发的特性,资源管理相对容易。但在手动创建任务时,要注意取消任务以释放资源。例如,在使用 URLSession 进行网络请求时,当任务取消时要取消相关的网络请求。

func sendCancelableRequest(url: URL) async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data, error == nil else {
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(throwing: NetworkError.unknown)
                }
                return
            }
            continuation.resume(returning: data)
        }
        task.resume()
        continuation.onCancel = {
            task.cancel()
        }
    }
}

在这个例子中,continuation.onCancel 确保在任务取消时,网络请求也被取消,避免资源浪费。

调试异步代码

调试 async/await 代码可能会有一些挑战,因为执行流程可能比较复杂。可以使用 print 语句在关键位置输出日志,或者使用 Xcode 的调试工具。例如,在异步函数的开始和结束处打印日志:

func complexAsyncFunction() async {
    print("Complex async function started")
    let result1 = await subTask1()
    let result2 = await subTask2(result1)
    // 处理结果
    print("Complex async function ended")
}
func subTask1() async -> Int {
    await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    return 42
}
func subTask2(_ value: Int) async -> Int {
    await Task.sleep(nanoseconds: 3 * 1_000_000_000)
    return value + 13
}

通过这些日志,可以更好地理解异步代码的执行流程。

与其他编程语言并发模型的对比

与 Java 的对比

Java 使用线程池、FutureCompletableFuture 等机制来实现并发编程。与 Swift 的 async/await 相比,Java 的语法相对复杂。例如,在 Java 中进行异步任务并获取结果:

import java.util.concurrent.*;
public class JavaAsyncExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Integer> future = executor.submit(() -> {
            // 模拟耗时操作
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 42;
        });
        Integer result = future.get();
        System.out.println("Result: " + result);
        executor.shutdown();
    }
}

而在 Swift 中使用 async/await 可以更简洁地实现相同功能:

func getResult() async -> Int {
    await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    return 42
}
func processResult() async {
    let result = await getResult()
    print("Result: \(result)")
}

Swift 的 async/await 语法使得异步代码更接近同步代码的写法,提高了代码的可读性。

与 Python 的对比

Python 使用 asyncio 库和 async/await 语法来实现异步编程。Python 的 asyncio 更侧重于事件循环驱动的异步模型。例如,在 Python 中:

import asyncio

async def get_result():
    await asyncio.sleep(2)
    return 42

async def process_result():
    result = await get_result()
    print("Result:", result)

if __name__ == "__main__":
    asyncio.run(process_result())

Swift 的结构化并发与 Python 的事件循环模型有所不同。Swift 的结构化并发通过任务的层次关系自动管理资源和取消操作,而 Python 的事件循环需要开发者更精细地管理任务的生命周期和资源。

总结

Swift 的 async/await 为并发编程带来了极大的便利,使得异步代码的编写更加直观和安全。通过结构化并发的设计,任务的管理和资源的释放变得更加容易。在实际开发中,合理运用 async/await 可以提高应用程序的性能和响应性,同时保持代码的清晰和可维护性。无论是网络请求、数据处理还是与其他异步编程模型的结合,async/await 都提供了强大的支持。在编写 async/await 代码时,要注意性能优化、资源管理和调试技巧,以充分发挥其优势。与其他编程语言的并发模型相比,Swift 的 async/await 具有简洁、直观的特点,为开发者带来了更好的编程体验。