As Sweeper said, the first three are working asynchronously (are called later). You want to check the results only after those finished. The typical GCD pattern to have the asynchronous work use a DispatchGroup, which can notify you went it’s done:
var valueOne = 1
var valueTwo = 10
var valueThree = 20
var valueFour = 30
let queue = DispatchQueue(label: "serialQueue")
let group = DispatchGroup()
DispatchQueue.main.async(group: group) { // loop 1
for _ in 0..<10 {
valueOne += 1
}
}
queue.async(group: group) { // loop 2
for _ in 0..<10 {
valueTwo += 1
}
}
queue.async(group: group) { // loop 3
for _ in 0..<10 {
valueThree += 1
}
}
for _ in 0..<10 { // loop 4
valueFour += 1
}
group.notify(queue: .main) {
print("\(valueOne),-- \(valueTwo),-- \(valueThree),--\(valueFour)")
}
That runloop trick that Sweeper shared with you is necessary if you are doing this GCD work in a command line app, but otherwise, DispatchGroup is the idiomatic pattern.
Please forgive the unrelated edits, but focus on the DispatchGroup and using notify to let you know when it is done.
This may be too much to take in at this point (and if so, set the following aside and come back to it later), but we should note a few problems in the above:
We are declaring mutable variables on one thread and updating them on another; this is a “data race”. We would make sure to manually synchronize our access to this shared mutable state. When you start to use Swift 6, the compiler will prevent you from using this brittle pattern (a shared mutable state without synchronization).
Nowadays, folks would use Swift concurrency (async-await), not GCD. The use of await eliminates the “how do I know when the asynchronous work is done” problem of your original question. No spinning on a run loop or dispatch group is required. And actors can be used to address the data race, too.
So, in Swift concurrency, to perform multiple tasks in parallel, we would use async let. And to know when the work was done, we would await the result:
@MainActor
func experiment() async {
let experimentActor = ExperimentActor()
async let valueOne = one() // loop 1
async let valueTwo = experimentActor.two() // loop 2
async let valueThree = experimentActor.three() // loop 3
var valueFour = 30 // loop 4
for _ in 0..<10 {
valueFour += 1
}
await print("\(valueOne),-- \(valueTwo),-- \(valueThree),--\(valueFour)")
}
For clarity’s sake, I’ve moved these first three loops into their own functions, the first isolated to the main actor, the latter two isolated to their own actor:
@MainActor
func one() async -> Int {
var value = 1
for _ in 0..<10 {
value += 1
}
return value
}
// The `ExperimentActor` is an `actor` that will let tasks 2 and 3 run concurrently
// with work on the main actor, but will make sure that 2 and 3 do not run in parallel
// with each other:
actor ExperimentActor {
func two() async -> Int {
var value = 10
for _ in 0..<10 {
value += 1
}
return value
}
func three() async -> Int {
var value = 20
for _ in 0..<10 {
value += 1
}
return value
}
}
Anyway, I added a little instrumentation to watch this in Instruments’ “Points of Interest” tool:

We can see that:
- The first task doesn’t run until the fourth loop finishes (because we are on the main actor, and the first task, also isolated to the main actor, doesn't have a chance to run until
experiment reaches its first suspension point, the await).
- The second task starts immediately.
- The third task starts after the second one finishes (because it was on the same actor as the second task).
- The work we ran synchronously (the fourth loop) starts immediately.
- That ⓢ is where we finished, after we awaited tasks 1-3.
This is thread-safe and uses await to know when all of our asynchronous work is done.
Now, I recognize that I have eliminated one aspect of your original question, namely how to have multiple threads mutating some shared state (your four variables). But thread-safe programming dictates that we never have different threads mutating some unsynchronized shared state (i.e., we avoid this “data race”). If that really was an important part of your question, I might suggest watching Protect mutable state with Swift actors and Eliminate data races using Swift Concurrency if you want to get your arms around this aspect of the language.
syncandasyncmethods onDispatchQueueare just that... methods from the Dispatch library. They don't really have anything to do with Swift specifically (the same methods are callable from C and Objective C, under similar names). I think you're confusing this with Swift'sasync/awaitfeature, like @loremipsum mentioned.