Is optional binding thread-safe?

The title says it in a nutshell. I want to operate on a variable which is an optional type. Another thread could set the variable to nil at any time, so I'd like to be sure to capture the contents before the variable is set to nil. I'm concerned that at the low level between the nil test and the copy into the inner scope, the variable might be nil'd.


DispatchQueue.asyncAfter( ... ) {

optionalStuff = nil

...other work...

}


Elsehwere in the same class:


if let safeStuff = optionalStuff {

... use safeStuff and know it is definitely not nil...

}

Answered by DTS Engineer in 299846022

You have no reasonable expectation of its being thread safe.

Agreed.

Because two values are being stored, there are no atomic accesses to the struct, and hence it is thread-unsafe.

But now you’re off in the weeds. Unless you’re writing assembly language, the underlying atomicity of a CPU access is irrelevant to thread safety. Thread safety is controlled by the language memory model.

Unfortunately Swift doesn’t have a defined language memory model, which makes this whole discussion very murky. The best you can get is SE-0176 Enforce Exclusive Access to Memory. This explicitly opts out of the threading issue but it does lay out a general guideline that I think is a safe assumption for the future: two accesses to the same variable are not allowed to overlap unless both accesses are reads.

Now consider a code snippet like this:

struct Test {

    var b: Bool = false

    mutating func willLoopForever() {
        self.b = false
        while !self.b {
            // spin
        }
    }
}

The compiler can optimise

willLoopForever()
by replacing it with an infinite loop. Why? Because:
  • It can prove that no code on the current thread can change

    b
    to
    true
  • No other thread can modify

    b
    because this thread is holding an exclusive access to
    self

These sorts of compiler optimisations can really ruin your day. There’s a classic series of articles that explains this in detail (start with What Every C Programmer Should Know About Undefined Behavior #1/3). While that is about C, it’s safe to assume that the Swift compiler implements similar optimisations.

In C-based languages you can fix this specific problem by declaring

b
as
volatile
, but that’s not an option in Swift. And even if it were, there’s an additional layer of complexity related to memory ordering. For example, C code like this:
volatile char b = 0;
volatile char * name = NULL;

void producer(void) {
    name = strdup("Hello Cruel World!");
    b = 1;
}

void consumer(void) {
    while (!b) {
        // spin
    }
    size_t l = strlen(name);
    printf("%zu\n", l);
}

is not safe because the C memory model does not define the order in which memory accesses are seen by another thread. That means it’s possible for the thread running

consumer()
to see the write to
b
made by the thread running
producer()
before it sees the write to
name
, and so it ends up calling
strlen
with
NULL
.

This is not a theoretical problem. The Arm architecture specifies a weak memory model and modern Apple CPUs are exploiting that fact more and more in order to boost performance by increasing instruction level parallelism. And given that the C memory model explicitly allows for this, I think it’s likely that the final Swift memory model will as well.

Given all of the above, here’s my advice:

  • If you share state between threads, make it immutable.

  • If that’s not possible — that is, you absolutely have to share mutable state between threads — use a concurrency primitive to guard that access.

  • Do not try to be clever. Avoid

    volatile
    , atomic accesses, and so on unless you have performance metrics that prove that the concurrency primitives you’re using are slowing you down significantly.

    IMPORTANT On modern systems concurrency primitives are really fast, so in most cases you just don’t have to be clever.

  • If you have to be clever, implement the clever parts of your code in a language that has a defined memory model.

  • Test with the thread sanitiser. The beauty of that tool is that it enforces the concurrency rules, rather than looking for specific problems exhibited by your current compiler and CPU, and thus it gives you some confidence that your code works today and will continue to work in the future.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Optional is immutable, meaning it is automatically thread safe.

Is optional binding thread-safe?
 
 
Q