Home > Enterprise >  Can I create a class instance using DispatchQueue.global().async and have its methods run asynchrono
Can I create a class instance using DispatchQueue.global().async and have its methods run asynchrono

Time:01-29

I created the playground below to answer my question "If I created a class instance using DispatchQueue.global().async would that class remain in its own asynchronous queue? Even if the main app called one of that classes methods, would that method run asynchronously compared to the main app?

With the sleep line I discovered that the answer is "no."

But I am curious if there is a legit way to do this? Or even if there is, it is considered bad programming?

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")

CodePudding user response:

You asked:

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

No.

Objects created on one queue can be accessed from other queues. The queue from which you create it has no bearing on the queue its methods use. Unless you have your type manually dispatch code to the appropriate queue (or use an actor), the methods will run on the caller’s queue.

There are a few issues with the provided code snippet:

  1. The code is not thread-safe, because it is updating a reference to a newly created object on a background thread while accessing the same reference from the current thread. E.g., if you put this in an app and turn on the data race

  2. You also have a logic “race” where you might end up creating multiple instances of your object: Your current thread might get through a few iterations before any of the background threads have a chance to update the object reference (because you’re doing that asynchronously). E.g., I ran the code (with 10 iterations), and we can see multiple “making” messages and an 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
    

    Now, these results will change from run to run (the hallmark of a good race; lol), but it illustrates the problem. Clearly, you were only dispatching the instantiation to another queue in the hopes that its methods would automatically use that thread (which is not the case). But once we fix the type to manually run its methods on the appropriate queue (see below), this “instantiate on a particular queue” is no longer an issue. One should just instantiate the object on the current queue before entering the loop.

  3. You also have “thread explosion”, where you are potentially dispatching 100 tasks to the global queue, which only has 64 worker threads. You should avoid unbridled dispatches to a global queue.

You then ask:

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

The initializing of an object, while you interact access it from another queue, is not a good practice. Instead, instantiate it before starting your work.

And if you want your object to perform some calculations on another thread, have your object create its own queue and use it from all of its methods. And, obviously, because these are asynchronous, you would want to give them completion handlers. E.g.,

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)
        }
    }
}

And then the caller would use it like so:

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")

Or, nowadays, we would use the Swift Concurrency system. So, we would create an actor, rather than a 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
    }
}

And

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")

In the above, I have kept it simple with a serial dispatch queue and an actor with synchronous methods. There are equivalent concurrent patterns for both.


By the way, you referred to using playgrounds. Since we are dealing with asynchronous code (i.e., tasks that will finish later), you either need to do this in a stand-alone app or, if you are going to use playgrounds, you have to tell your playground page that it needsIndefiniteExecution:

import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
  •  Tags:  
  • Related