Post

Replies

Boosts

Views

Activity

Async function doesn’t see external changes to an inout Bool in Release build
Title Why doesn’t this async function see external changes to an inout Bool in Release builds (but works in Debug)? Body I have a small helper function that waits for a Bool flag to become true with a timeout: public func test(binding value: inout Bool, timeout maximum: Int) async throws { var count = 0 while value == false { count += 1 try await Task.sleep(nanoseconds: 0_100_000_000) if value == true { return } if count > (maximum * 10) { return } } } I call like this: var isVPNConnected = false adapter.start(tunnelConfiguration: tunnelConfiguration) { [weak self] adapterError in guard let self = self else { return } if let adapterError = adapterError { } else { isVPNConnected = true } completionHandler(adapterError) } try await waitUntilTrue(binding: &isVPNConnected, timeout: 10) What I expect: test should keep looping until flag becomes true (or the timeout is hit). When the second task sets flag = true, the first task should see that change and return. What actually happens: In Debug builds this behaves as expected: when the second task sets flag = true, the loop inside test eventually exits. In Release builds the function often never sees the change and gets stuck until the timeout (or forever, depending on the code). It looks like the while value == false condition is using some cached value and never observes the external write. So my questions are: Is the compiler allowed to assume that value (the inout Bool) does not change inside the loop, even though there are await suspension points and another task is mutating the same variable? Is this behavior officially “undefined” because I’m sharing a plain Bool across tasks without any synchronization (actors / locks / atomics), so the debug build just happens to work? What is the correct / idiomatic way in Swift concurrency to implement this kind of “wait until flag becomes true with timeout” pattern? Should I avoid inout here completely and use some other primitive (e.g. AsyncStream, CheckedContinuation, Actor, ManagedAtomic, etc.)? Is there any way to force the compiler to re-read the Bool from memory each iteration, or is that the wrong way to think about it? Environment (if it matters): Swift: [fill in your Swift version] Xcode: [fill in your Xcode version] Target: iOS / macOS [fill in as needed] Optimization: default Debug vs. Release settings I’d like to understand why Debug vs Release behaves differently here, and what the recommended design is for this kind of async waiting logic in Swift.
2
0
1.1k
Nov ’25