To simplify my problem: I have a huge calculation function with 3 inputs and 2 outputs. My input variables are @Published
SwiftUI variables inside a SwiftUI ObservableObject
. The UI does modify these input variables quite often, if the user takes actions. Output variables are also @Published
variables inside the same class. As said, the background calculation is huge – therefore, I need to run the calculation all the time when one of the three inputs changes. If the inputs change during calculation, a new calculation is needed with the current (new) values. This should be as efficient and performant as it gets.
I come from Android programming and in Java I created a very efficient solution by having a background thread that runs all the time, sleeps when no new calculation is needed and gets woken up (notified in Java language) when one of the inputs changed. It then calculates the new results by request the most-current input parameters. If these input parameters changed during calculation, the thread does another calculation with the most-current, and so on…
But, I’m not sure how to achieve this in Swift. I see we can submit tasks to GCD on an extra queue for background processing. So, it is not possible to have a background thread running, being paused and waiting for new inputs, and calculating them – correct? I would need to send calculation tasks to the GCD queues all the time. I read and tried around a lot and created my first solution, which works but doesn’t work well and efficient, I think:
class ViewModel: ObservableObject, CalculatorDelegate {
var calculator = Calculator()
@Published var input1: Date
@Published var input2: Int
@Published var input3: Bool
init() {
calculator.delegate = self
$input1.sink { [weak self] in
self?.calculator.requestUpdate()
}.store(...)
$input2.sink { [weak self] in
self?.calculator.requestUpdate()
}.store(...)
$input3.sink { [weak self] in
self?.calculator.requestUpdate()
}.store(...)
}
func createRequest() -> Data.Request { return Data.Request(input1, input2, input3) }
func setResult(result: Data) { ... }
}
And the calculator itself:
class Calculator {
var delegate: CalculatorDelegate?
private static let queue = DispatchQueue(label: "asdf", qos: .userInteractive)
private let blInstance = BL()
private var reqForUpdate = false
func register(for requestProvider: CalculatorDelegate) {
self.delegate = requestProvider
}
func requestUpdate() {
if !reqForUpdate {
reqForUpdate = true
scheduleUpdate()
}
}
private func scheduleUpdate() {
Calculator.queue.async { [weak self] in
if let request = self?.delegate?.createRequest() {
self?.reqForUpdate = false
let result = self?.blInstance.calc(request: request)
if let result = result {
DispatchQueue.main.async {
self?.delegate?.setResult(result: result)
}
}
}
}
}
}
protocol CalculatorDelegate {
func createRequest() -> Data.Request
func setResult(result: Data)
}
Is this a good strategy? I think it’s too complicated and it does not seem to be as efficient as the Android version I described above (having a endless running calculation background thread waiting for new data or processing as long as new data is there). Furthermore, is GCD the right tool here to use? I’m not sure…
I simplified the code to underline the important parts. Thanks a lot for your help or hints.
2
Answers
You asked:
GCD does something very similar to this: It has a pool of “worker threads”, and when you dispatch something to a queue, it just grabs an available worker thread to run the dispatched code. This eliminates the overhead of spinning up a new thread. This results in a highly performant multi-threading mechanism.
GCD abstracts the developers away from managing threads and avoids the creation and destruction of threads unnecessarily. Just use GCD (or operation queues or
async
–await
) and enjoy efficient thread utilization.GCD works, but I would also consider the
async
–await
concurrency system or operation queues.Specifically, a key design requirement is that you want to be able to cancel your calculation (so you can presumably start another). GCD’s
DispatchWorkItem
supports cancelation, but operation queues andasync
–await
arguably handle this more elegantly. So, while you could use GCD, I would personally suggestasync
–await
if supported by my target OS (e.g., iOS 13 and later), and use operation queue if I needed to support older operating systems.But whether dispatch queue, operation queue, or
async
–await
, you will want to participate in “cooperative cancelation”. Specifically, you will have your time-consuming calculation periodically check the respectiveisCancelled
property, exiting the calculation iftrue
.Your use of Combine’s
ObservableObject
andsink
for the inputs is highly non-standard. That should only be used when you want toassign
the output of a Combine pipeline to an@Published
so isn’t the right approach here. I recommend a standard SwiftUI approach like this async calculator example below. Note the use of@Binding
and.task(id:)
.