并发

异步使命对于期望开释资源让体系能够履行其它使命的场景非常有用,比方更新界面,但在期望同步履行两个使命时,就需求用到并发。为此,Swift规范库界说了async let句子。将异步使命变成多个并发使命,咱们只需求运用async let句子声明处理,如下所示。

示例9-8:界说并发使命

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
                .padding()
        }
        .onAppear {
            let currentTime = Date()
            Task(priority: .background) {
               async let imageName1 = loadImage(name: "image1")
               async let imageName2 = loadImage(name: "image2")
               async let imageName3 = loadImage(name: "image3")
                let listNames = await "(imageName1), (imageName2), (imageName3)"
                print(listNames)
                print("Total Time: (Date().timeIntervalSince(currentTime))")
            }
        }
    }
    func loadImage(name: String) async -> String {
        try? await Task.sleep(nanoseconds: 3 * 1000000000)
        return "Name: (name)"
    }
}

每次完结async let所声明的处理,体系会创立一个并发使命与其它使命一同并行运转。在示例9-8中,咱们创立了三个并发使命(imageName1imageName2imageName3)。过程与之前相同,它们调用loadImage()办法,方向会暂停使命3秒钟,回来一个字符串。但由于这次它们并行运转,完结使命所花费的时刻大约为3秒(而不是前例中的9秒)。

✍️跟我一同做:运用示例9-8中的代码更新ContentView结构体。在模拟器中运转代码。几秒后,会在操控台中打印出处理所耗费的时刻。

Actor

在运用并发使命时,或许会碰到数据竞用的问题。数据竞用呈现在两个或两个以上并行运转的使命测验拜访相同的数据时。比方,它们一同测验修正某一个特点的值。这或许会导致过错或严重的bug。为解决这一问题,Swift规范库中引入了actor

actor是阻隔并行使命的数据类型,因而使命在修正actor的值时,另一个使命会强制等候。actor是引证类型,界说相似类,但不是运用class关键字,而是经过actor关键字界说。它与类另一个重要的不同是特点和办法有必要异步拜访(咱们有必要运用await关键字等候)。这会保证代码等候actor开释(其它使命不能拜访actor)。

下例演示了怎么运用actor。这段代码声明了一个一家特点和办法的actor,创立了一个实例,然后在多个使命中调用其间的办法。

示例9-9:界说一个actor

import SwiftUI
actor ItemData {
    var counter: Int = 0
    func incrementCount() -> String {
        counter += 1
        return "Value: (counter)"
    }
}
struct ContentView: View {
    var item: ItemData = ItemData()
    var body: some View {
        Button("Start Process") {
            Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (timer) in
                Task(priority: .background) {
                    async let operation = item.incrementCount()
                    print(await operation)
                }
            }
            Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { (timer) in
                Task(priority: .high) {
                    async let operation = item.incrementCount()
                    print(await operation)
                }
            }
        }
    }
}

界面中的按钮会启动两个无限重复的定时器,一个距离0.1秒,另一个距离0.2秒。定时器履行使命并发调用actor中的incrementCount()办法。这样不同线程中的不同使命会调用该办法,最终会一同调用,发生数据竞用。假如咱们将ItemData声明为类,会报错、呈现预期外的行为乃至呈现崩溃,但由于咱们将这个数据类型声明为actor,代码正确运转。每次在使命调用incrementCount()办法时,actor会接管并保证一次只要一个使命能拜访该办法。

✍️跟我一同做:运用示例9-9中的代码更新ContentView.swift文件。在iPhone模拟器中运转代码、点击按钮。会看到incrementCount()办法所发生的值打印在操控台中。中止运用。将actor声明为类(将关键字actor替换成关键字class)。这时在办法一同由多个使命调用时会呈现过错。

留意:默许Xcode不会在操控台显现异步过错。要监测异步操作的问题,有必要激活Thread Sanitizer。点击Xcode工具栏的Scheme按钮(图5-2,2号图)。在菜单中选择Edit Scheme选项(图5-8)。在新窗口中,选择Run选项并翻开Diagnostics标签。勾选复选框启用Thread Sanitizer。将actor声明为类,再次在iPhone模拟器中运转运用,点击按钮。在成功运转数次后,会在操控台中看到拜访竞争的报错。

咱们提到过,actor将特点和办法与其它的代码及线程隔脱离。这表明咱们只能异步拜访actor(有必要等候actor答应进行拜访),但在某些场景下,不需求进行阻隔。这时,咱们能够运用如下的关键字反转阻隔的状况。

  • nonisolated:该关键字打破特点或办法的阻隔。

非阻隔特点和办法或许遵从协议时要用到,也能够在actor中只需求拜访不可变值时简化代码。例如,下例中咱们对ItemData actor增加一个名为maximum的常量以及一个打印该值的办法。由于常量的值永不改变,咱们能够将其声明为非阻隔办法,调用时无需等候actor授权。

示例9-10:界说一个非阻隔办法

actor ItemData {
    var counter: Int = 0
    let maximum: Int = 30
    func incrementCount() -> String {
        counter += 1
        return "Value: (counter)"
    }
    nonisolated func maximumValue() -> String {
        return "Maximum Value: (maximum)"
    }
}
struct ContentView: View {
    var item: ItemData = ItemData()
    var body: some View {
        Button("Start Process") {
            let value = item.maximumValue()
            print(value)
        }
    }
}

在前面的例子中,咱们操作了actor所界说的值,但一般值也会发送给actor进行处理。将值发送给actor中的办法非常风险。由于actor的使命是保证两个或多个异步使命不能同步修正同一个值。值类型,包括自界说类型和IntString这样的原生数据类型,是线程安全的,由于会进行值复制。在运用这些值调用actor中的办法时,体系创立一个复制并将复制发送给办法,所以不会修正原始值。但目标是引证类型,所以发送给actor的是目标的指针,也就意味着目标或许会在代码的其它当地被修正,存在数据竞用的或许。为保证咱们发送给办法的值是安全的,Swift规范库界说了如下协议和特点。

  • Sendable:这一协议告知体系该数据类型创立的值可安全地在异步线程间同享。
  • @Sendable:该特点向体系表明某个办法或闭包可安全地在异步线程间同享。

Sendable协议没做什么工作,仅仅告知编译器某一数据类型是安全的。在数据类型遵从该协议时,其间包括不安全的值时编译器就会报错。比方,尽管结构体是安全的,但咱们能够让其遵从这一协议来保证之后不会增加任何不安全的特点。在只包括不可变值时类也是安全的,但子类却有或许不安全,因而能够运用final关键字来标记类,这样没人能够创立其子类,如下所示。

示例9-11:界说非阻隔办法

final class Product: Sendable {
    let name: String
    init(name: String) {
        self.name = name
    }
}
actor ItemData {
    var stock: Int = 100
    func sellProduct(product: Product, quantity: Int) {
        stock = stock - quantity
        print("Stock: (stock) (product.name)")
    }
}
struct ContentView: View {
    var item: ItemData = ItemData()
    var body: some View {
        Button("Start Process") {
            Task(priority: .background) {
                let product = Product(name: "Lamp")
                await item.sellProduct(product: product, quantity: 5)
            }
        }
    }
}

上例中界说了一个名为Product的final类(不能对其创立子类),其间包括一个不可变特点(let)。一同,这个特点的类型为String,默许为可发送类型。这表明经过该类创立的目标是线程安全的,可发送给actor

留意:带有可变值(var)的类也能够是可发送的,但咱们需求担任保证它不会发生数据竞用。这一话题暂不在评论领域内。更多相关多信息,请拜见本文的参考链接部分。

假如确实需求包括不安全的值,而且确认不会在其它线程中修正它,能够经过如下特点告知编译器不做过错查看。

  • @unchecked:该特点要求编译器不查看指定数据类型是否遵从Sendable协议。

在运用不安全的数据类型或是向actor发送老框架所发生的值时这比较有用。例如,下例中咱们将Product类转换成结构体,运用name特点存储NSString值。NSString数据类型是不可发送的,因而Product结构体不遵从Sendable协议的要求,但由于咱们知道这个值不会在任何当地进行修正,所以经过@unchecked特点告知编译器不要担心这个问题。

示例9-12:要求编译器不查看是否遵从Sendable协议

struct Product: @unchecked Sendable {
    let name: NSString
}

留意@unchecked特点一般完结用于在将不安全的值发送给Main Actor前封装它。咱们会在下一节学习怎么运用Main Actor,以及在稍后在实际场景中完结这一特点。

Main Actor

咱们现已讲到,使命会分配给履行线程,然后体系将这些线程分发到处理器的多核,赶快尽或许滑润地履行使命。一个线程可办理多个使命,一个运用可创立多个线程。除了为处理异步、并发使命初始化的那些线程,体系还会创立一个称为主线程的线程,用于启动运用和运转非异步代码,包括创立和更新界面的代码。这表明假如测验经过异步或并发使命修正界面,或许会导致数据竞用或是严重的bug。为防止这类冲突,Swift规范库界说了Main ActorMain Actor是由体系创立的actor,用于保证每个期望与主线程交互或是修正界面元素的使命等候其它使命完结。Swift供给了两种方式来保证代码运转于Main Actor(主线程):@MainActor修饰符和run()办法。经过@MainActor修饰符咱们能够标记整个办法运转于主线程上,而run()办法在主线程上运转闭包。例如,下例中咱们运用@MainActor标记loadImage()办法,来保证其间的代码在主线程中运转,而且咱们修正Text视图的值时不会呈现问题。

示例9-13:在Main Actor中履行办法

struct ContentView: View {
    @State private var myText: String = "Hello, world!"
    var body: some View {
        VStack {
            Text(myText)
                .padding()
        }.onAppear {
            Task(priority: .background) {
                await loadImage(name: "image1")
            }
        }
    }
    @MainActor func loadImage(name: String) async {
        myText = name
    }
}

这段代码和之前相同创立了一个异步使命,但这里办法运用@MainActor进行标记,因而代码在主线程中运转,能够安全地更新myText特点及界面。

大多数时候,只要一部分代码处理界面,但其它代码可在当前线程中履行。这时,咱们能够完结run()办法。这是一个由MainActor结构体(用于创立Main Actor)界说的类型办法。该办法接纳一个包括需求在主线程中运转的句子的闭包。

示例9-14:在Main Actor中履行代码

struct ContentView: View {
    @State private var myText: String = "Hello, world!"
    var body: some View {
        VStack {
            Text(myText)
                .padding()
        }.onAppear {
            Task(priority: .background) {
                await loadImage(name: "image1")
            }
        }
    }
    func loadImage(name: String) async {
        await MainActor.run {
            myText = name
        }
        print(name)
    }
}

loadImage()的最终包括了一条句子,在操控台打印出字符串,但只要将新值赋给myText特点的句子需求在主线程中运转,因而咱们把它放到了run()办法中。留意这个办法运用await进行了标记。需求用await的原因是该办法需求等候主线程闲暇才干履行该句子。

run()办法也能够回来值。这对于进行杂乱运算后陈述成果比较有用。咱们只需求记住有必要声明闭包所回来值的类型,如下所示。

示例9-15:经过Main Actor回来值

    func loadImage(name: String) async {
        let result: String = await MainActor.run {
            myText = name
            return "Name: (name)"
        }
        print(result)
    }

异步序列

有时信息以值的序列回来,但这些值并不是马上安排妥当。这时,咱们能够创立一个异步序列。这种序列相似数组,但值是异步回来的,因而咱们有必要等候每次值都安排妥当。Swift规范库包括两个创立异步序列的协议:用于界说序列的AsyncSequence协议以及用于界说代码遍历序列以回来值的AsyncIteratorProtocol协议。AsyncSequence协议要求数据类型包括一个类型别号Element,表明序列回来的数据类型,以及下面这个办法。

  • makeAsyncIterator() :该办法回来杂乱生成值的迭代器实例。回来的值是遵从AsyncIteratorProtocol协议的数据类型的实例。

AsyncIteratorProtocol协议只要求数据类型完结如下办法。

  • next() :该办法回来列表中的下一个元素。该办法被重复调用,直至回来的值是表明序列完毕的nil

要创立异步序列,咱们有必要界说两个数据类型,一个遵从AsyncSequence,用于描绘序列回来值的数据类型并初始化迭代器,另一个遵从AsyncIteratorProtocol协议,用于生成值。在下例中,咱们界说了一个异步序列,逐一处理字符串数组并回来一个String序列。

示例9-16:界说一个异步序列

struct ImageIterator: AsyncIteratorProtocol {
    let imageList: [String]
    var current = 0
    mutating func next() async -> String? {
        guard current < imageList.count else {
            return nil
        }
        try? await Task.sleep(nanoseconds: 3 * 1000000000)
        let image = imageList[current]
        current += 1
        return image
    }
}
struct ImageLoader: AsyncSequence {
    typealias Element = String
    let imageList: [String]
    func makeAsyncIterator() -> ImageIterator {
        return AsyncIterator(imageList: imageList)
    }
}
struct ContentView: View {
    let list = ["image1", "image2", "image3"]
    var body: some View {
        VStack {
          Text("Hello World!")
                .padding()
        }.onAppear {
            Task(priority: .background) {
                let loader = ImageLoader(imageList: list)
                for await image in loader {
                    print(image)
                }
            }
        }
    }
}

示例9-16中的代码模拟从网上异步下载图片。首先界说带next()办法的迭代器。在这个办法中,咱们从list数组中读取字符串,并更新计数器来确认是否到达末尾(计算器的值等于或大于数组的元素数量时)。

接着由ImageLoader结构体界说异步序列。这个结构体包括一个类型别号Element,表明序列回来String值,还有一个makeAsyncIterator()办法用于初始化迭代器。

准备好读取序列中的值后,咱们启动一个使命,创立ImageLoader序列的实例,然后运用for in循环遍历元素。留意for in循环要求用await关键字等候序列中的每个元素。循环一直运转到迭代器回来nil为止。

✍️跟我一同做:运用示例9-16中的代码更新ContentView.swift文件。在模拟器中运转运用。会看到每3秒在操控中打印list数组中的值。

使命组

使命组是一个动态生成使命的容器。组创立好后,咱们能够经过代码按运用要求增加和办理使命。Swift规范库界说了如下用于创立组的大局办法。

  • withTaskGroup(of: Type, returning: Type, body: Closure):该办法创立一个使命组。of参数界说由使命回来的数据类型,returning参数界说由组回来的数据类型,body参数是一个界说使命的闭包。假如没有要回来的值,能够疏忽这些参数。
  • withThrowingTaskGroup(of: Type, returning: Type, body: Closure):该办法界说一个能够抛出过错的组of参数界说由使命回来的数据类型,returning参数界说由组回来的数据类型,body参数是一个界说使命的闭包。假如没有要回来的值,能够疏忽这些参数。

组由TaskGroup结构体的实例界说,包括在组中办理使命的特点和办法。以下是一些最常用的特点和办法。

  • isCancelled:该特点回来一个表明组是否被撤销的布尔值
  • isEmpty:该特点回来一个表明组是否还有使命的布尔值。
  • addTask(priority: TaskPriority?, operation: Closure):该办法向组增加使命。priority参数是帮助体系决议何时履行使命的结构体。该结构体包括预界说规范权重的类型特点。当前能够运用的有backgroundhighlowmediumuserInitiatedutilityoperation参数是一个包括使命所履行句子的闭包。
  • cancelAll() :该办法撤销组中的一切使命。

使命组是使命的异步序列。序列为泛型,也便是使命和组能够回来恣意类型的值。这也是为什么创立使命组需求两个参数,一个用于指定使命回来的数据类型,另一个指定组回来的数据类型。

创立使命组的两个办法也相同。咱们完结哪个取决于是否期望抛出过错。这些办法创立一个TaskGroup结构体,运用参数所指定的数据类型,并将实例回来给闭包。运用闭包中的值,咱们能够向组增加恣意需求增加的使命,如下所示。

示例9-17:界说使命组

struct ContentView: View {
    var body: some View {
        VStack {
          Text("Hello World!")
                .padding()
        }.onAppear {
            Task(priority: .background) {
                await withTaskGroup(of: String.self) { group in
                    group.addTask(priority: .background) {
                        let imageName = await self.loadImage(name: "image1")
                        return imageName
                    }
                    group.addTask(priority: .background) {
                        let imageName = await self.loadImage(name: "image2")
                        return imageName
                    }
                    group.addTask(priority: .background) {
                        let imageName = await self.loadImage(name: "image3")
                        return imageName
                    }
                    for await result in group {
                        print(result)
                    }
                }
            }
        }
    }
    func loadImage(name: String) async -> String {
        try? await Task.sleep(nanoseconds: 3 * 1000000000)
        return "Name: (name)"
    }
}

本例中,咱们创立了一个不抛出过错的使命组。这个组也不回来值,但使命回来字符串,所以咱们将withTaskGroup()办法的of参数声明为String数据类型(String.self)。使命逐一增加到组中。每个使命履行与前面相同的处理。它们异步调用loadImage()办法,获取回来的字符串。

由于使命组是一个异步使命序列,咱们能够运用for in循环遍历其间的值,这与前一节对所创立的异步序列操作相同。每次使命完结时,组回来由使命发生的值直至没有使命,这时会回来nil完毕循环。

留意:使命组以序列存储使命。咱们能够删除、过滤乃至是查看组中是否包括指定的使命。这一话题暂不做评论,请拜见本章的参考链接部分。

异步图画

尽管本章所介绍的工具可用于履行各种类型的异步或并发使命,Swift还是单独地供给了AsyncImage视图简化咱们处理图画的操作。这个视图担任从服务端下载图画并在安排妥当时在屏幕上显现图画。以下是最常用的初始化办法。

  • AsyncImage(url: URL, scale: CGFloat):该视图从服务端下载图画并在屏幕上显现。url参数是一个带图画url的URL结构体,scale参数是咱们期望对图画赋的扩大份额(默许值是1)。
  • AsyncImage(url: URL, scale: CGFloat, content: Closure, placeholder: Closure):
  • 该视图从服务端下载图画并在屏幕上显现。url参数是一个带图画url的URL结构体,scale参数是咱们期望对图画赋的扩大份额(默许值是1),content参数是处理图画的闭包,placeholder是在等候图画下载回来在图画的当地显现视图的闭包。

图片的位置由URL结构体决议。这些结构体用于存储长途地址和本地文档、文件及资源。以下是创立拜访文档和网络资源URL所需求的初始化办法。

  • URL(string: String):运用string参数指定URL创立URL结构体的初始化办法。
  • URL(string: String, relativeTo: URL?):经过参数指定URL创立URL结构体的初始化办法。将string参数值增加到relativeTo参数值来创立URL
  • URL(dataRepresentation: Data, relativeTo: URL?, isAbsolute: Bool):经过参数指定URL创立URL结构体的初始化办法。将dataRepresentation参数的值增加到relativeTo参数值来创立URLisAbsolute是一个布尔值,指定URL是否是绝对链接(包括拜访资源所需的一切信息)。

有两种类型的URL:安全的和不安全的。不安全的URL运用http协议(超文本传输协议)标识,安全的URL运用https协议(超文本安全传输协议)标识。默许答应安全URL,但假如需求翻开不安全的URL,有必要装备运用绕过Apple设备完结的称为ATS(App Transport Security)的安全体系。

装备ATS体系的装备项为App Transport Security Settings,经过Info面板增加至运用装备。咱们介绍过这个面板(图5-13),运用它增加自界说字体(图5-34)。之前也讲过,新选项经过右侧的+按钮增加。

大师学SwiftUI第9章Part 2 - 异步并发之Actor、异步序列、使命组和异步图画

图9-2:增加装备项的按钮

点击+按钮(图9-2圈出的部分)后,在选项下面会增加一个空的文本框。输入文字会在下拉框中显现可用的选项,可经过列表进行选择。

大师学SwiftUI第9章Part 2 - 异步并发之Actor、异步序列、使命组和异步图画

图9-3:运用传输安全选项

App Transport Security Settings选项仅仅一个容器。要装备这个选项,咱们有必要增加子项。点击左边的箭头增加子项(图9-3圈出的部分),然后再次点击+按钮。答应运用翻开不安全的URL的选项为Allow Arbitrary Loads

大师学SwiftUI第9章Part 2 - 异步并发之Actor、异步序列、使命组和异步图画

图9-4:装备运用传输安全答应拜访不安全URL

Allow Arbitrary Loads接纳由字符串YESNO(或是10)指定的布尔值。将其设置为YES1)答应翻开恣意URL。假如期望只答应指定域名,有必要运用Exception Domains,增加期望包括的域名。这一子项又至少三个子项,键名分别为NSIncludesSubdomains(布尔值)、NSTemporaryExceptionAllowsInsecureHTTPLoads(布尔值)和NSTemporaryExceptionMinimumTLSVersion(字符串)。例如以下的装备答应翻开alanhou.org域名下的文档。

大师学SwiftUI第9章Part 2 - 异步并发之Actor、异步序列、使命组和异步图画

图9-5:装备运用传输安全答应翻开来自alanhou.org的文档

装备运用传输安全体系是否必要取决于咱们期望用户能够拜访的URL类型。默许答应安全URL,但假如期望用户拜访不安全的URL,有必要增加运用装备选项,如上图所示。例如,下例加载了来自本站不安全版别的图片(http协议)。

示例9-18:异步加载图片

struct ContentView: View {
    let website = URL(string: "https://www.6hu.cc/wp-content/uploads/2023/11/220375-NE3hNO.jpg")
    var body: some View {
        VStack {
            AsyncImage(url: website)
        }.padding()
    }
}

AsyncImage视图下载并显现图片只需求传一个URL。本例中,咱们将URL存储在常量中,然后完结加载图画的视图。尽管有效加载并显现了图片,AsyncImage视图并不答应做任何装备,因而图片按原始大小进行显现。

大师学SwiftUI第9章Part 2 - 异步并发之Actor、异步序列、使命组和异步图画

图9-6:异步加载图片

假如期望装备图片,有必要为content参数供给一个闭包。这个闭包接纳一个Image视图,能够像之前那样经过视图修饰符进行装备。

示例9-19:装备下载完结的图片

struct ContentView: View {
    let website = URL(string: "http://alanhou.org/homepage/wp-content/uploads/2019/03/201903251411121.jpg")
    var body: some View {
        VStack {
            AsyncImage(url: website, content: { image in
                image
                    .resizable()
                    .scaledToFit()
            }, placeholder: {
                Image(.nopicture)
            })
            Spacer()
        }.padding()
    }
}

在供给了content参数后,AsyncImage视图会将图片显现的使命交给经过闭包所接纳的Image视图,所以咱们能够像之前相同装备该视图。本例中,咱们运用resizable()修饰符重置图片的大小,经过scaleToFit()修饰符缩放图片适配在视图之内。留意咱们还界说了placeholder参数在图片下载过程中作为临时显现图片。

大师学SwiftUI第9章Part 2 - 异步并发之Actor、异步序列、使命组和异步图画

图9-7:图片装备

✍️跟我一同做:创立一个多渠道项目。下载nopicture.png,增加至Asset Catalog。运用示例9-19中的代码更新ContentView视图。点击顶部的导航区翻开运用的装备面板(图5-4,编号6)。翻开info面板,按照图9-29-39-4所示的步骤操作。数秒后应该会看到nopicture.png被咱们装备的图片替换掉。

代码请见:GitHub仓库

本文首发地址:AlanHou的个人博客,收拾自2023年10月版《SwiftUI for Masterminds》