我可以使用 DispatchQueue.global().async 创建一个 class 实例并使其方法 运行 异步吗?

Can I create a class instance using DispatchQueue.global().async and have its methods run asynchronously?

我创建了下面的 playground 来回答我的问题“如果我使用 DispatchQueue.global().async 创建了一个 class 实例,那么 class 会保留在它自己的异步队列中吗?即使主应用程序调用其中一种 classes 方法,与主应用程序相比,该方法 运行 会异步吗?

sleep 行中,我发现答案是“否”。

但是我很好奇是否有合法的方法可以做到这一点?或者即使有,也被认为是糟糕的编程?

import UIKit

class MyFunx : NSObject {
    var opsCount = 0
    
    override init() {
        super.init()
    }
    
    func addValues (a: Int, b: Int) {
        let c = a + b
        opsCount += 1
        sleep(1)
    }
}

var firstVar    = 0
var secondVar   = 0

var myFunx : MyFunx?

while secondVar < 100 {
    print ("starting")
    if myFunx == nil {
        print ("making myFunx")
        DispatchQueue.global().async {
            myFunx = MyFunx()
         }
    } else {
        myFunx!.addValues(a: firstVar, b: secondVar)
    }
    firstVar += 1
    secondVar += 1
}
print ("myFunx = \(myFunx)")
print ("\(myFunx?.opsCount)")
print ("exiting")

你问过:

If I created a class instance using DispatchQueue.global().async would that class remain in its own asynchronous queue?

没有

在一个队列上创建的对象可以从其他队列访问。您创建它的队列与其方法使用的队列无关。除非您让您的类型手动将代码分派到适当的队列(或使用 actor),否则这些方法将 运行 在调用者的队列中。

提供的代码片段存在一些问题:

  1. 代码不是 thread-safe,因为它正在更新对后台线程上新创建对象的引用,同时从当前线程访问相同的引用。例如,如果您将其放入应用程序并打开 Thread Sanitizer (TSAN),它将报告:

  2. 您还有一个逻辑“竞赛”,您可能最终会创建对象的多个实例:您当前的线程可能会在任何后台线程有机会更新之前经历几次迭代对象引用(因为您是异步执行的)。例如,我 运行 代码(迭代 10 次),我们可以看到多个“making”消息和 opsCount of 6:

    starting
    making myFunx
    starting
    making myFunx
    starting
    making myFunx
    starting
    starting
    starting
    starting
    starting
    starting
    starting
    myFunx = Optional(<MyApp.MyFunx: 0x6000019e03b0>)
    opsCount = Optional(6)
    exiting
    

    现在,这些结果将从 运行 变为 运行(良好比赛的标志;大声笑),但它说明了问题。显然,您只是将实例化分派到另一个队列,希望它的方法会自动使用该线程(事实并非如此)。但是一旦我们将类型修复为在适当的队列上手动 运行 它的方法(见下文),这种“在特定队列上实例化”就不再是问题了。在进入循环之前,应该只实例化当前队列中的对象。

  3. 您还遇到了“线程爆炸”,您可能将 100 个任务分派到只有 64 个工作线程的全局队列。您应该避免对全局队列进行肆无忌惮的分派。

你接着问:

I am curious if there is a legit way to do this?

在您从另一个队列交互访问对象时初始化对象不是一个好的做法。相反,在开始工作之前实例化它。

如果您希望您的对象在另一个线程上执行一些计算,请让您的对象创建自己的队列并从其所有方法中使用它。而且,显然,因为它们是异步的,所以您会希望为它们提供完成处理程序。例如,

class Foo {
    private var operationsCount = 0                      // note, make this private; caller should use `fetchOperationsCount`
    private let queue = DispatchQueue(label: "Foo")

    /// Asynchronously add two values
    ///
    /// - Parameters:
    ///   - a: First value
    ///   - b: Second value
    ///   - completion: Completion handler called asynchronously with result

    func add(a: Int, b: Int, completion: @escaping (Int) -> Void) {
        queue.async {
            let c = a + b
            self.operationsCount += 1
            Thread.sleep(forTimeInterval: 1)
            completion(c)
        }
    }

    /// Asynchronously fetch the operations count
    ///
    /// - Parameter block: Asynchronous block called with count.

    func fetchOperationsCount(block: @escaping (Int) -> Void) {
        queue.async {
            block(self.operationsCount)
        }
    }
}

然后调用者会像这样使用它:

var a = 0
var b = 0

let foo = Foo()

while b < 10 {
    print("iteration", b)
    foo.add(a: a, b: b) { [b] sum in                     // capture copy of `b`
        print("total after \(b): \(sum)")
    }
    a += 1
    b += 1
}

foo.fetchOperationsCount { count in
    print("ops count \(count)")
}
print ("exiting")

或者,现在,我们会使用 Swift Concurrency system. So, we would create an actor,而不是 class:

actor Foo {
    private var operationsCount = 0                      // note, make this private; caller should use `fetchOperationsCount`

    func add(a: Int, b: Int) -> Int {
        Thread.sleep(forTimeInterval: 1)
        operationsCount += 1
        return a + b
    }

    func fetchOperationsCount() -> Int {
        return operationsCount
    }
}

var a = 0
var b = 0

let foo = Foo()

while b < 10 {
    print("iteration", b)
    Task { [a, b] in                                     // capture copies of `a` and `b`
        let sum = await foo.add(a: a, b: b)
        print("total after \(b): \(sum)")
    }
    a += 1
    b += 1
}

Task {
    let count = await foo.fetchOperationsCount()
    print("ops count \(count)")
}
print ("exiting")

在上面,我使用串行调度队列和具有同步方法的 actor 来保持简单。两者都有等效的并发模式。


顺便说一句,您提到了使用游乐场。由于我们正在处理异步代码(即稍后将完成的任务),您需要在 stand-alone 应用程序中执行此操作,或者,如果您要使用游乐场,则必须告诉您的游乐场页面它needsIndefiniteExecution:

import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true