iOS学习

Swift5.5 并发初探

2021-07-05  本文已影响0人  Flum_X

异步函数

Swift has built-in support for writing asynchronous and parallel code in a structured way. … the term concurrency to refer to this common combination of asynchronous and parallel code.

       Swift官方文档是这样描述Swift并发的,它指的就是异步和并行代码的组合。并行编程需要解决的主要问题:

为了更容易和更优雅的解决上面两个问题,在Swift5.5中,引入了异步函数的概念。在函数声明的返回箭头前面,加上async关键字,就可以把一个函数声明为异步函数:

func loadSignature() async -> String {
    fatalError("暂未实现")
}

async关键字会帮助编译器做两件事情:

代码举例

需求:从服务器拉取100000条天气数据,求取这些数据的平均值,然后将平均值回传给服务器。

分析:请求服务器的操作都是异步的毋庸置疑,由于数据量过大,求取平均值是个耗时操作,也应该异步处理。

常规代码实现:

func fetchWeatherHistory(completion: @escaping ([Double]) -> Void) {
    // 用随机值来取代网络请求返回的数据
    DispatchQueue.global().async {
        let results = (1...100_000).map { _ in Double.random(in: -10...30) }
        completion(results)
    }
}

func calculateAverageTemperature(for records: [Double], completion: @escaping (Double) -> Void) {
    // 先求和再计算平均值
    DispatchQueue.global().async {
        let total = records.reduce(0, +)
        let average = total / Double(records.count)
        completion(average)
    }
}

func upload(result: Double, completion: @escaping (String) -> Void) {
    // 省略上传的网络请求代码,均返回"OK"
    DispatchQueue.global().async {
        completion("OK")
    }
}

调用实现

fetchWeatherHistory { [weak self] records in
    self?.calculateAverageTemperature(for: records) { average in
        self?.upload(result: average) { response in
            print("Server response: \(response)")
        }
    }
}

存在的问题:

async/await实现代码

func fetchWeatherHistory() async -> [Double] {
    (1...100_000).map { _ in Double.random(in: -10...30) }
}

func calculateAverageTemperature(for records: [Double]) async -> Double {
    let total = records.reduce(0, +)
    let average = total / Double(records.count)
    return average
}

func upload(result: Double) async -> String {
    "OK"
}

调用实现

func processWeather() async {
    let records = await fetchWeatherHistory()
    let average = await calculateAverageTemperature(for: records)
    let response = await upload(result: average)
    print("Server response: \(response)")
}

仅仅通过async关键字将函数标记为异步返回值,在调用函数前加上await关键字,让整个调用过程变得简单清晰,就像在编写同步代码一样。

调用流程对比

普通函数的调用流程:(如上图)

这里普通函数放弃线程控制权的唯一方式就是执行完成

异步函数的调用流程:(如上图)

这里需要注意几点:

异步属性

       在Swift5.5中,升级了只读属性,以单独或一起支持asyncthrows关键字,使它们更灵活。

enum FileError: Error {
    case missing, unreadable
}

struct BundleFile {
    let filename: String

    var contents: String {
        get async throws {
            guard let url = Bundle.main.url(forResource: filename, withExtension: nil) else {
                throw FileError.missing
            }

            do {
                return try String(contentsOf: url)
            } catch {
                throw FileError.unreadable
            }
        }
    }
}

因为contents属性同时是asyncthrows,读取时必须使用try await:

func printHighScores() async throws {
    let file = BundleFile(filename: "highscores")
    try await print(file.contents)
}

注意点:

结构化并发

对于同步函数来说,线程决定了它的执行环境。而对于异步函数,则由任务(Task)决定执行环境。Swift提供了一系列Task相关API来让开发者创建、组织、检查和取消任务。这些API围绕着Task这一核心类型,为每一组并发任务构建出一棵结构化的任务树:

这些特性看上去和Operation类有一些相似,不过Task直接利用异步函数的语法,可以用更简洁的方式进行表达。而Operation则需要依靠子类或者闭包。

在调用异步函数时,需要在它前面添加await关键字;而另一方面,只有在异步函数中,我们才能使用 await关键字。那么问题在于,第一个异步函数执行的上下文,或者说任务树的根节点,是怎么来的?

简单地使用Task.init就可以让我们获取一个任务执行的上下文环境,它接受一个async标记的闭包:

struct Task<Success, Failure> where Failure : Error {
    init(
        priority: TaskPriority? = nil, 
        operation: @escaping @Sendable () async throws -> Success
    )
}

它继承当前任务上下文的优先级等特性,创建一个新的任务树根节点,我们可以在其中使用异步函数:

var results: [String] = []

func someSyncMethod() {
    Task {
        try await processFromScratch()
        print("Done: \(results)")
    }
}

func processFromScratch() async throws {
    let strings = await loadFromDatabase()
    if let signature = try await loadSignature() {
        strings.forEach {
            results.append($0.appending(signature))
        }
    } else {
        //throw error
    }
}

processFromScratch中的处理依然是串行的:对loadFromDatabaseawait将使这个异步函数在此暂停,直到实际操作结束,接下来才会执行loadSignature

task-serial.png

我们当然会希望这两个操作可以同时进行,同时,只有当两者都准备好后,才能调用appending来实际将签名附加到数据上。这需要任务以结构化的方式进行组织。使用async let绑定可以做到这一点:

 func processFromScratchNew() async throws {//结构化并发
     async let loadStrings = loadFromDatabase()
     async let loadSignature = loadSignature()
        
     let strings = await loadStrings
     if let signature = try await loadSignature {
         strings.forEach {
             results.append($0.appending(signature))
         }
     } else {
         //throw error
     }
 }

async let被称为异步绑定,它在当前Task上下文中创建新的子任务,并将它用作被绑定的异步函数的运行环境。和Task.init新建一个任务根节点不同,async let所创建的子任务是任务树上的叶子节点,它是结构化的。被异步绑定的操作会立即开始执行,即使在await之前执行就已经完成,其结果依然可以等到 await语句时再进行求值。在上面的例子中,loadFromDatabaseloadSignature将被并发执行。

除了async let外,另一种创建结构化并发的方式,是使用任务组(Task group)。比如,我们希望在执行 loadResultRemotely的同时,让processFromScratch一起运行,可以将两个操作写在同一个task group中:

func someSyncMethod() {
    Task {
        await withThrowingTaskGroup(of: Void.self) { group in
            group.async {
                try await self.loadResultRemotely()
            }
            group.async {
                try await self.processFromScratch()
            }
        }          
        print("Done: \(results)")
    }
}

演员模型

Swift5.5引入了actor,在概念上类似于在并发环境中可以安全使用的类,即需要确保在任何时间只能由单个线程访问actor内的可变状态。

代码演示:创建一个RiskyCollector类,该类能够实现两个收集器对象之间交换牌组中的卡片。

class RiskyCollector {
    var deck: Set<String>

    init(deck: Set<String>) {
        self.deck = deck
    }

    func send(card selected: String, to person: RiskyCollector) -> Bool {
        guard deck.contains(selected) else { return false }

        deck.remove(selected)
        person.transfer(card: selected)
        return true
    }

    func transfer(card: String) {
        deck.insert(card)
    }
}

在单线程中,代码是安全的,但是在多线程中就不安全了,如果我们同时调用send(card:to:)多次,可能会发生以下事件链:

在这种情况下,一个玩家失去1张牌,而另一个玩家得到2张牌,这显然是不合理的。
通过actor模型可以解决这个问题:除非异步执行,否则无法从Actor对象外部读取存储的属性和方法,并且根本无法从 Actor 对象外部写入存储的属性。异步行为不是为了性能;相反,这是因为Swift会自动将这些请求放入一个按顺序处理的队列中,以避免出现竞争条件。因此,我们可以将RiskyCollector类重写为SafeCollectoractor,如下所示:

actor SafeCollector {
    var deck: Set<String>

    init(deck: Set<String>) {
        self.deck = deck
    }

    func send(card selected: String, to person: SafeCollector) async -> Bool {
        guard deck.contains(selected) else { return false }

        deck.remove(selected)
        await person.transfer(card: selected)
        return true
    }

    func transfer(card: String) {
        deck.insert(card)
    }
}

注意点:

需要明确的是,actor可以自由地、异步或以其他方式使用自己的属性和方法,但是当与不同的actor交互时,它必须始终异步完成。通过这些更改,Swift可以确保永远不会同时访问所有与actor隔离的状态,更重要的是,这是在编译时完成的,以保证安全。

Actor和类的对比,相同点:

区别:

总结

Swift并发的概念很多,但是各种的模块边界是清晰的:

熟悉这些边界,有助于我们清晰地理解 Swift 并发各个部分的设计意图,从而让我们手中的工具可以被运用在正确的地方。

上一篇下一篇

猜你喜欢

热点阅读