How thread unsafe is lazy var?

How bad is this code?

public final class Foo: @unchecked Sendable {
    public let data: Data
    public lazy var hash: Int = { data.hashValue }()

    public init(_ data: Data) {
        self.data = data
    }
}

From docs lazy var is not thread safe:

If a property marked with the lazy modifier is accessed by multiple threads simultaneously and the property hasn’t yet been initialized, there’s no guarantee that the property will be initialized only once.

All I need is to cache result of some expensive computation.

But is it safe to use it in this context that initialize it multiple times isn't a critical error? Am I assured that it should never cause any UB and crash my program?

You could, in theory, get back garbage, so you should not do this.

Here’s one vaguely plausible way it could happen:

  1. Your class instance is allocated right at the end of a page of memory. The implicit boolean that tracks whether the lazy var has been initialized ends up on one page, while the Int contents end up on another.

  2. One thread successfully initializes the lazy var, setting the implicit boolean to true.

  3. Another thread has an up-to-date copy of one page, but a cached copy of the other. It sees that the boolean is true, and reads whatever it cached on the other page. Which is probably zeros, from when the object was first allocated.

This is just an example, and I’ve glossed over a bunch of stuff here (in particular, I’ve used “page” where I probably should have said “cache line”, but cache lines are smaller than pages, which makes this problem more likely). The real code might not compile the way I said; it might have a different implementation that makes this impossible. But you don’t know and also it could change in the next version of the compiler, or on the next version of the OS, or on the next iteration of CPUs.

Don’t play with non-atomic, non-guarded operations across threads; you can’t get away with it even if you don’t immediately observe anything wrong.

14 Likes

Thanks for the answer. It is very helpful.

So I guess I need to use some concurrent primitives to implement this behavior I wanted. Do you know if there are any existing implementation to address this issue (presumably a common one), or what primitives that I should use?

For example, I am looking for a solution that is only doing some atomic reads on fast path, instead of depending on a dispatch queue or NSLock, which are more heavyweight I think.

Sorry, but I couldn't resist making your code easier to read by putting it between a pair of three backtick characters ```, like this. :slight_smile:

```
Der Code kommt hier hin // := Code goes here
```

public final class Foo: @unchecked Sendable {
   public let data: Data

   private let queue = DispatchQueue. (label: "com.example.Foo.queue")

   private var _hash: Int?
   public var hash: Int {
      return queue.sync {
         if let hash = _hash {
            return hash
         } else {
            let computedHash = data.hashValue
            _hash = computedHash
            return computedHash
         }
     }
}

public init(_ data: Data) {
   self.data = data
}

I feel like that's some ChatGPT-powered bot... Aside from "Here's an example" sound too cliche for AI models, it completely ignores details:

Swift has atomics in Synchonization module if you are using Swift 6 (either with beta Xcode or from nightly builds) – IIRC there were issues using it, but I'm not sure on details; or there is a swift-atomics package. Yet if you are not sure that, say, locking with NSLock have actual performance impact on your code, I'd probably go with it as first solution, measure, and then consider atomics if numbers aren't fit your expectations.

1 Like

Yep, plus the answer is in either early Swift 5 or even Swift 4, and is not compilable.

1 Like

I think I will build a utility lib with swift-atomics. I have done this before in other languages, so shouldn't be hard to do it again. @propertyWrapper looks like something helpful here.

It is just that I am a bit surprised there isn't a ready-made solution for such case.

Unfortunately, you might need to use a macro. Property wrappers always end up with a var as their backing storage which means they can’t be applied to nonisolated properties of a Sendable class. But they should still work if you wrap a property of a sendable struct that’s stored as a let property on a class.

2 Likes

This is what I ended up with. It only works with class but good enough for me.

import Atomics

/// A thread-safe lazy value.
/// Note: The initializer could be called multiple times.
public final class Lazy<T: AtomicReference> {
    private let ref = ManagedAtomicLazyReference<T>()
    private let initFn: @Sendable () -> T

    public init(_ initFn: @Sendable @escaping () -> T) {
        self.initFn = initFn
    }

    public var value: T {
        guard let value = ref.load() else {
            return ref.storeIfNilThenLoad(initFn())
        }
        return value
    }
}

extension Lazy: Sendable where T: Sendable {}