Initialization of a variable in a sync task

Hello. I have long wanted to ask why it is impossible to initialize a variable in a sync task.

import Dispatch

struct Raillway {
    let queue = DispatchQueue(label: "Redway")
    func run() {
        let z: Int
        queue.sync {
            z = 1 // Cannot assign to value: 'z' is a 'let' constant
        }
        check(p: z)
    }
    func check(p: Int) {}
}

There is the error. But why? It is obvious the code will not progress until the variable is initialized.

You defined z as a constant. That’s why you cannot modify it later. You can either define the value of the constant at the point of creation, or make z a variable (using var instead of let) if you want its value to change later.

This doesn't work

struct Raillway {
    let queue = DispatchQueue(label: "Redway")
    func run() {

        queue.sync {
            let n = 1
        }
        check(p: n) // Cannot find 'n' in scope
    }
    func check(p: Int) {
        print("p: \(p)")
    }
}

That looks like a bug, You're using z outside of the scope that it's declared, and you're uaing z inside z, which is not defined in the scope. Maybe you can file a bug report?

Aside from that, passing it through queue.sync may be more idiomatic:

let z = q.sync {
  1
}

Sorry, my z is the global variable
let z = Int.init(20)

Don't you mean to use the local one when calling check? This is just a straight up shadowing bug.

Yes. Now it does not compile

You're using z outside of its declared scope. Not compiling is a very good thing there.

Yes it is. But anyway there is synchronization queue trouble as I described in first post

DispatchQueue.sync takes a regular non-escaping closure, so there's hardly anyway to do precisely as you want as the compiler doesn't know if it'll be call exactly once. What about what I suggested:

or a slightly more complex way:

let (y, z) = queue.sync {
  (1, 2)
}

It works, but I don't understand how

DispatchQueue.sync returns whatever the closure returns. And a closure with only 1 expression will return the result of that expression.

2 Likes

It's not obvious to the compiler.
queue.sync{ [...] } is just an API call to the compiler with a non-escaping closure.
The compiler does not know that the closure is executed exactly once.
It must assume that the closure is not called at all or multiple times.
That's why you can not use it to initialise a constant.

That is a known limitation and one reason for the generic version of sync:

func sync<T>(execute work: () throws -> T) rethrows -> T

This allows you to initialise your constant like this:

let z: Int = queue.sync { 1 }

The implementation of this function could look something like this:

extension DispatchQueue {
    func sync<T>(execute work: () -> T) -> T {
        var returnValue: T?
        self.sync {
            returnValue = work()
        }
        return returnValue!
    }
}
1 Like