Swift concurrency
Categories:
To commence, I must acknowledge that my proficiency in Swift concurrency is limited. While I possess a rudimentary comprehension of the subject matter, if you are perusing this blog and seeking further elaboration on the topic, I strongly recommend conducting a search and perusing articles from alternative sources that offer a more comprehensive understanding of Swift concurrency.
RsyncUI is a graphical user interface (GUI) application; the majority of its operations are executed on the main thread. However, some resource-intensive tasks are performed on other threads managed by the cooperative thread pool (CTP), excluding and not blocking the main thread. How to the executors and CTP works and interacts is details I dont know about, and it is managed by the Swift runtime. There are three kinds of executors:
- the main executor manage jobs on the main thread
- the global concurrent executor and the serial executor, both executes jobs on threads from the CTP
Most work in RsyncUI are executed on the main thread. And by default, SwiftUI makes sure all UI-updates are performed on the main thread. Below are some tasks within RsyncUI, which are executed on the main thread:
- preparing of and execution of
rsync
synchronize tasks, preparing is computing the correct arguments for rsync - monitoring progress and termination of the real rsync tasks
- monitoring progress is an asynchronous sequence running on the main thread, by asynchronous sequence of two specific notifications generated by the notification center
- some write and read operations
Swift version 6 and the new concurrency model
Swift version 6 introduced strict concurrency checking. By enabling Swift 6 language mode and strict concurrency checking, Xcode assists in identifying and resolving possible data races at compile time.
Quote swift.org: “More formally, a data race occurs when one thread accesses memory while the same memory is being modified by another thread. The Swift 6 language mode eliminates these issues by preventing data races at compile time.”
RsyncUI adheres to the new concurrency model of Swift 6.
Swift concurrency and asynchronous execution
Concurrency and asynchronous execution are important parts in Swift. The latest version of Swift simplifies the writing of asynchronous code using Swift async
and await
keywords, as well as the actor
protocol. RsyncUI does not require concurrency, but concurrency is automatically introduced by using actors
, async
and await
keywords. It is an objective to execute most work synchronous on the main thread as long as it does not block the GUI.
Asynchronous execution can be performed on both the main thread and background threads from the CTP. When executing asynchronous operations on the main thread, it is crucial to utilize Swift’s structured concurrency, specifically the async/await keywords. The await keyword serves as a suspension point, allowing other and more critical tasks to access the main thread.
Cooperative thread pool (CTP)
The following tasks are executed asynchronous on threads from the CTP, adhering to the actor protocol:
- read synchronize tasks from file
- JSON data decoding, asynchronous decoding which inherits the thread from the actor reading data
- JSON data encoding, synchronous encoding on the main thread
- read and sort log records
- preparing output from rsync for display
- preparing data from the logfile for display
- checking for updates to RsyncUI
Adhering to the actor protocol, all access to properties within an actor must be performed asynchronously. There are only five actors in RsyncUI. But there are more asynchronous functions, some are running on the main thread as well.
Structured concurrency
Some concurrent functions within RsyncUI are structured by using async let
. You may have several async let
, and they will all be executed in parallel or concurrent. When all async let
tasks are completed, the root task or parent task, will continue execution.
Structured concurrency might also dictate the order of execution. The keyword await
is a suspension where execution waits until the asynchronous function is completed before continuing the execution. If there are several await
after each other, the next one will be executed when the current asynchronous task is completed.
func readconfigurations() {
Task {
let monitornetworkconnection = SharedReference.shared.monitornetworkconnection
let sshport = SharedReference.shared.sshport
let actorReadSynchronizeConfigurationJSON = ActorReadSynchronizeConfigurationJSON()
async let data = actorReadSynchronizeConfigurationJSON
.readjsonfilesynchronizeconfigurations(profile,
monitornetworkconnection,
sshport)
rsyncUIdata.configurations = await data
}
}
Unstructured concurrency
The code snippet below presents an unstructured concurrency. The code within the Task {}
does not have a parent/child relationship to the caller. And it may be completed either before or after the execution of the calling function, the parent, is completed.
@MainActor
func somefunction() {
// Some code
Task {
newversion.notifynewversion = await GetversionofRsyncUI().getversionsofrsyncui()
}
// Some code
}
The primary reason for utilizing an actor is to execute the task on a separate thread from the CTP, thereby preventing blocking of the main thread. The calling function operates on the main thread, while the asynchronous function executes on a background thread. In the event that the remote call takes an extended period or times out, the UI remains unblocked. The call retrieves a file from GitHub to determine whether a new version of RsyncUI is available.
actor GetversionofRsyncUI {
@concurrent
nonisolated func getversionsofrsyncui() async -> Bool {
do {
let versions = await DecodeGeneric()
if let versionsofrsyncui =
try await versions.decodearraydata(VersionsofRsyncUI.self,
fromwhere: Resources().getResource(resource: .urlJSON))
{
let runningversion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
let check = versionsofrsyncui.filter { runningversion.isEmpty ? true : $0.version == runningversion }
if check.count > 0 {
return true
} else {
return false
}
}
} catch {
return false
}
return false
}
}