Swift 不知道 Actor 是什么任务?

问题描述 投票:0回答:1

我正在学习

async/await
和任务。所以我了解到任务基本上继承了演员。 想象我有一个模型:

class SomeModel: ObservableObject {
   @Published var downloads: [Int] = []

   func doSome() async throw {
      // MAKE URL
      downloads.append(1)

      try await /// MAKE REQUEST
      downloads.append(data) // after request
    
   }
}

我有一个简单的视图

struct DownloadView: View {

  @EnvironmentObject var model: SomeModel


  var body: some View {
    List {
      // USE model.downloads 
    }
    .task {
        do {
          try await model.doSome()
        } catch {}
    }
}

我收到一个错误,提示我正在从后台线程更新 UI。没关系。 然后我添加

func doSome() async throw {
      // MAKE URL
      downloads.append(1)

      try await /// MAKE REQUEST
      await MainActor.run {
            downloads.append(data)
       } // after request
}

我仍然有错误。系统不知道第一个追加总是主角?或者这是来自傻瓜的安全措施,如果某些人在第一次追加之前有一些暂停的代码?

我如何理解系统需要分层等待才能知道哪些子作业树需要暂停。但是暂停点是在第一次追加之后,所以它不能传递给不同的演员。

重现代码: 型号

struct DownloadFile: Identifiable {
    var id: String { return name }
    let name: String
    let size: Int
    let date: Date
    
    static let mockFiles = [DownloadFile(name: "File1", size: 100, date: Date()),
                            DownloadFile(name: "File2", size: 100, date: Date()),
                            DownloadFile(name: "File3", size: 100, date: Date()),
                            DownloadFile(name: "File4", size: 100, date: Date()),
                            DownloadFile(name: "File5", size: 100, date: Date()),
                            DownloadFile(name: "FIle6", size: 100, date: Date())]
    
    static let empty = DownloadFile(name: "", size: 0, date: Date())
}

初见

struct ContentView: View {
    @State var files: [DownloadFile] = []
    let model: ViewModel
    
    @State var selected = DownloadFile.empty {
      didSet {
        isDisplayingDownload = true
      }
    }
    
    @State var isDisplayingDownload = false
    
    var body: some View {
      NavigationStack {
        VStack {
          // The list of files available for download.
          List {
            Section(content: {
              if files.isEmpty {
                ProgressView().padding()
              }
              ForEach(files) { file in
                Button(action: {
                  selected = file
                }, label: {
                    Text(file.name)
                })
              }
            })
          }
          .listStyle(.insetGrouped)
        }
        .task {
            try? await Task.sleep(for: .seconds(1))
            files = DownloadFile.mockFiles
        }
        .navigationDestination(isPresented: $isDisplayingDownload) {
          DownloadView(file: selected).environmentObject(model)
        }
      }
    }
}

第二个视图

struct DownloadView: View {
    let file: DownloadFile
    @EnvironmentObject var model: ViewModel
    @State var result: String = ""
    var body: some View {
        VStack {
            Text(file.name)
            if model.downloads.isEmpty {
                Button {
                    Task {
                        result = try await model.download(file: file)
                    }
                } label: {
                    Text("-- Download --")
                }
            }
            Text(result)
        }
    }
}

视图模型

class ViewModel: ObservableObject {
    @Published var downloads: [String] = []
    
    func download(file: DownloadFile) async throws -> String {
        downloads.append(file.name)

        try await Task.sleep(for: .seconds(2))
        return "Download Finished"
    }
}
swift swiftui async-await task actor
1个回答
0
投票

系统不知道第一个追加总是主角?

“系统”(或更准确地说是编译器)此时应该如何知道这一点? Swift 并发中没有魔法可以识别,如果您从特定参与者访问一个代码位置中的属性,则对该属性的所有访问都应受到该参与者的保护。

在您的情况下,

SomeModel
未绑定到特定参与者,
doSome
函数也未绑定到特定演员。 任务修饰符创建一个新的顶级任务,如果您不将其绑定到特定参与者,则会遇到所描述的问题。

子任务继承其所属的顶级任务的参与者,但对于顶级任务,您必须专门指定一个参与者。

所以你实际上应该做的是将你的视图模型绑定到主要参与者:

@MainActor
final class SomeModel: ObservableObject {
    // ...
}

这意味着对所有属性的访问都受到

MainActor
的保护,并且任何从其他参与者访问属性的尝试都必须作为
async
调用进行。然后,编译器会强制执行此操作,并明确此时必须进行特殊的受保护调用。 粗略地说,此时就好像您必须等待锁,不同之处在于调用线程不会因此被阻塞。

或者,如果您确定自己在做什么,只需将

doSome
方法和
downloads
属性绑定到
MainActor
并确保对
SomeModel
的所有其他访问都受到相应保护:

final class SomeModel: ObservableObject {
    @MainActor @Published private(set) var downloads: [Int] = []

    // ...

    @MainActor
    func doSome() async throw {
        // ...
    }
}

乍一看,下面列出的实现具有相当明显的并发问题:

func doSome() async throw {
      // MAKE URL
      downloads.append(1)

      try await /// MAKE REQUEST
      await MainActor.run {
            downloads.append(data)
       } // after request
}

由于该方法的类未绑定到任何参与者,因此异步

doSome
函数几乎可以由任何任务(因此线程)调用。

这意味着该方法也可以同时调用多次,并且在第一行中您已经对

downloads
进行了并发访问,这是一个问题,因为此时没有任何东西可以保护
downloads

虽然您对

downloads
的第二次访问是在
MainActor
上进行的,但其他任务仍可能再次并发调用
doSome
并在此方法的第一行中访问
downloads

我通常建议激活“严格并发检查”选项,这将在编译时检测到大量并发问题并发出错误。

请参阅此处此处

此模式也可以使用 Swift 5 激活,但不会显示与 Swift 6 模式一样多的可能错误。

© www.soinside.com 2019 - 2024. All rights reserved.