Thread-safe progress reporting in Swift

16 hours ago 2
ARTICLE AD BOX

In a Swift codebase, I have classes Progress and ProgressGroup that are used to track progress of a long-running operation.

A ProgressGroup can contains several child Progress objects. A Progress object is passed to a function that takes a long time to execute. This function calls progress.setProgress(0.5) on it, to indicate that it has finished 50%.

The Progress object then calls a function setChildProgress() on its parent ProgressGroup. If the ProgressGroup has 3 child Progress objects, that are at 0.0, 0.5, 0.0, its total progress is 0.16.

The ProgressGroup is also @Observable, so that it can be connected to the UI, or to another part that tracks it.

The code is more or less like this:

@MainActor @Observable class ProgressGroup { var childProgress: [Float] var children: [Progress]! = [] var totalProgress: Float = 0.0 init (numChildren: Int) { self.childProgress = Array(repeating: 0.0, count: numChildren) self.children = (0..<numChildren).map { idx in Progress(group: self, index: idx) } } func setChildProgress(idx: Int, progress: Float) { self.childProgress[idx] = progress var total: Float = 0.0 for childProgress in self.childProgress { total += childProgress } self.totalProgress = total / Float(self.children.count) // triggers observers } } @MainActor class Progress { let group: ProgressGroup let index: Int init (group: ProgressGroup, index: Int) { self.group = group self.index = index } func setProgress(progress: Float) { self.group.setChildProgress(idx: self.index, progress: progress) } }

Currently they are @MainActor so setChildProgress() can never be called concurrently from two threads.

But in the program, is should become possible for Progress objects to be passed to functions that will call it from different concurrent tasks (running in different actors). Without main actor isolation, it would produce race conditions, because ParentGroup.setChildProgress() would get called from different threads, which both modify ParentGroup.totalGroup.

I'm wondering what would be a good way in Swift to make this work.

One way would be to use a Mutex in ProgressGroup, as would be done in C++ or other. But Swift seems to discourage this.


But using Swift concurrency, it seems that the only way would be to make ProgressGroup an actor, and then use await group.setChildProgress().

But this would create many actors in the program (according to other sources it is recommended to only use a few actors, for services in the program that run concurrently). Also it forces all functions that do Progress.setProgress() calls to be async, and then also all of these function's callers.

Is there a good way to implement this in Swift?

Read Entire Article