Home > database >  Swift: thread-safe initialization of the reference type stored property
Swift: thread-safe initialization of the reference type stored property

Time:04-20

Lets say we have a class property, that is also a class:

class A { }

class B {
   public var a: A // we want to init it in thread-safe manner
}

// Because 2 threads may be doing this
let b = B()
DispatchQueue.concurrentPerform(iterations: 3) { _ in
    let x = b.a // expecting all threads to get the same instance of the property (i.e. same physical address in memory)
}

I know that lazy initialization is not thread-safe, but how about other options?

For instance:

1. Is there a difference between initializing property inline vs. in the init?

I.e. is:

class B {
   public var a: A = A()
}

any better or worse than

class B {
   public var a: A

   init() {
       a = A()
}

from thread-safety perspective?

2. Is the init of the property itself thread-safe?

or do we need to wrap it in some sort of instance getter (Java-style) like

extension A {
    static var newInstance -> A {
        return A()
    }
}
class B {
    public var a: A = A.newInstance
}  

(Java rationale was: in this case the pointer is not returned until the instance is created, where in case of direct init, the pointer is assigned while init is still running).

Note: yes, I did some testing. And I do not get "crashes" / don't see any problems or differences with either method above - seems like all three options above are equal (from empirical testing perspective). But maybe there's a more definitive answer somewhere.

CodePudding user response:

Is there a difference between initializing property inline vs. in the init?

No, there's no meaningful difference between assigning to a property inside of an init or providing it a default value outside of an init. Default properties are assigned immediately before initializers are called, so

class X {
    var y = Y()
    var z: Z

    init(z: Z) {
        self.z = z
    }
}

is conceptually equivalent to

class X {
    var y: Y
    var z: Z

    func _assignDefaultValues() {
        y = Y()
    }

    init(z: Z) {
        _assignDefaultValues()
        self.z = z
    }
}

which is equivalent to

class X {
    var y: Y
    var z: Z

    init(z: Z) {
        y = Y()
        self.z = z
    }
}

In other words, by the time the end of an init(...) is reached, all stored properties must be fully initialized, and there is no difference between having initialized them with a default value, or explicitly.


Is the init of the property itself thread-safe?

Teasing this apart, I believe there are two components to this question:

  1. "By the time init() returns, is b.a guaranteed to be assigned to?", and
  2. "If so, is the assignment guaranteed to be done in a way that other threads reading the value will be guaranteed to read a value that matches the assigned value, and that matches what other threads see?", i.e., reading the value without tearing?

The answer to (1) is yes. The Swift language guide covers the specifics in great detail, but has this to say specifically:

Classes and structures must set all of their stored properties to an appropriate initial value by the time an instance of that class or structure is created. Stored properties can’t be left in an indeterminate state.

This means that by the time you are able to read a b out of

let b = B()

b.a must have been assigned a valid A value.

The answer to (2) is a bit more nuanced. Typically, Swift does not guarantee thread-safe or atomic behavior in the default case, and there is no documentation that I could find, or references in the Swift source code which indicate that Swift make any promises as to atomic assignment to Swift properties during initialization. Although it's impossible to prove a negative, I think it's relatively safe to say that Swift does not guarantee that you get consistent behavior across threads without explicit synchronization.

However, what is guaranteed is that for the lifetime of b, it has a stable address in memory, and at that, b.a will have a stable address as well. At least part of the reason that your original code snippet appears to work in this specific case is that

  1. All threads are reading from the same address in memory,
  2. On many (most?) platforms that Swift supports, word-size (32 bits on 32-bit platforms; 64 bits on 64-bit platforms) reads and writes are atomic, and not susceptible to tearing (reading only part of a value out of a variable before another part is written to it) — and pointers in Swift are word sized. This does not guarantee that reads and writes will be synchronized across threads as you expect, but you won't get invalid addresses this way. But,
  3. Your code creates and assigns b.a before the other threads are ever spawned, which means that the assignment to b.a is much more likely to "go through" before they ever read from that memory

If you were to start assigning to b.a after spawning the concurrentPerform(iterations:), then all bets would be off because you'd have unsynchronized reads and writes interleaving in unexpected ways.

In general:

  1. Creating read-only data and passing it off to multiple threads isn't safe, but will typically work as expected in practice (but should not be relied upon!),
  2. Creating read-write data and passing off references to multiple threads isn't safe, and concurrent mutations also will not work as expected, and
  3. If you need a guarantee for safe atomic handling of variables and synchronization, it's recommended you use synchronization mechanisms like locks or atomics (e.g. from the official swift-atomics) package

When in doubt, too, it's recommended you run your code through the sanitizer tools offered by LLVM through Xcode — specifically, the Address Sanitizer to catch any memory-related issues, and in this case, the Thread Sanitizer as well, to help capture synchronization issues and race conditions. While not perfect, these tools can help give confidence that your code is correct.

  • Related