How to write an atomic property wrapper for static variable?

@Storage private static var basket: Set =

the error message:
Static property 'basket' is not concurrency-safe because it is nonisolated global shared mutable state

I cannot use nonisolated(unsafe) on property wrappers.

If I remove static, the codes below works correctly.


import os
@available(iOS 18.0, *)
@propertyWrapper
final class Storage<T>: Sendable where T: Hashable & Sendable {
    typealias MyState = Set<T>

    private let protectedState = OSAllocatedUnfairLock(initialState: MyState())

    init(wrappedValue: MyState) {
        
        protectedState.withLock { state in
            state = wrappedValue
        }
    }

    var wrappedValue: MyState {
        get {
            protectedState.withLock { $0 }
        }
        set {
            protectedState.withLock { state in
                state.formUnion(newValue)
            }
        }
    }
}


@available(iOS 18.0, *)
public class Basket {
    @Storage private static var basket: Set<String> = []
}

This has been discussed before: Static property wrappers and strict concurrency in 5.10

The reason for that error is that the compiler doesn't "know" that your internal state is protected and is safe to use, since OS atomics are not part of structured concurrency.

It seems like there are very few options to make it work but a property wrapper isn't one of them.

What I usually do when I want to have a safe static value under Swift 6 is something like this. The Storage type here uses a semaphore to free us from requiring an init constructor for the generic type:

final class Storage<T>: @unchecked Sendable {

	init(_ initialValue: T) {
		storage = initialValue
	}

	var value: T {
		get {
			withLock { $0 }
		}
		set {
			withLock {
				$0 = newValue
			}
		}
	}

	private func withLock<R>(result: (inout T) -> R) -> R {
		sem.wait()
		defer { sem.signal() }
		return result(&storage)
	}

	private let sem = DispatchSemaphore(value: 1)
	private var storage: T
}

Usage:

class C {
	static let c = Storage<Set<String>>([]) // this is fine

	static func test() {
		c.value = ["one", "two"]
		print(c.value)
	}
}

Notice the let in the static var declaration: it works since it's an object reference to an "artificially" sendable class, and that's what makes the compiler happy here. Artificially because it's not really sendable from the structured concurrency point of view, but it's still fine since we provide the safety mechanism using a semaphore.

1 Like

Is this different than Mutex?

import typealias Synchronization.Mutex

@propertyWrapper struct Storage<Element: Hashable & Sendable>: Sendable, ~Copyable {
  typealias WrappedValue = Set<Element>
  private let _wrappedValue: Mutex<WrappedValue>

  init(wrappedValue: WrappedValue) { _wrappedValue = .init(wrappedValue) }

  var wrappedValue: WrappedValue {
    get { _wrappedValue.withLock(\.self) }
    nonmutating set { _wrappedValue.withLock { $0.formUnion(newValue) } }
  }
}

You can basically still use it as a property wrapper.

public enum Basket {
  private static let _basket = Storage<String>(wrappedValue: [])
  static var basket: Set<String> {
    get { _basket.wrappedValue }
    set { _basket.wrappedValue = newValue }
  }
}
1 Like

Not very different, I don't think so. Depending on the OS, either mutexes are implemented using semaphores, or the other way around, or sometimes there's something else (like condvar) that is the basis for all other synchronization primitives, so in the end they are all the same.

Importantly, these implementations are not lock free. OS atomics, by contrast, can be lock free (and now that depends on the hardware platform), so if you worry about the performance, i.e. your static variable is small and is accessed often and from a lot of places, then use the OS atomics instead.

Also, I'd say your version with a non-copyable struct is probably a bit better than having an unchecked sendable class.

tangential to the core discussion here, but since this came up recently, using a struct property wrapper to wrap values in a mutex probably does not do what one generally wants it to.

The property wrapper’s backing storage is always a var and there is no way to change this, so you cannot use a property wrapper to protect shared mutable state (either as a static variable or as a Sendable class stored property).

I would like to suggest abandoning property wrapper completely and use OSAllocatedUnfairLock. There is the reason why many such types aren’t implemented as property wrappers and that’s because they not 100% atomic. Simplest case is counters you want to increment:

counter += 1

In fact this is 2 operations: getting current value of a counter and then writing new one, making this change no longer atomic.

If you're able to use Swift 6, I would recommend using the standard Mutex type over any manually-written wrappers. Even if it were possible to back a property wrapper with a let storage property, I would still strongly recommend against trying to hide the lock-taking behavior, since it is otherwise impossible to reason about the actual atomicity of updates. You can explicitly write:

private static let basket: Mutex<Set<T>> = Mutex([])

and update that value from any thread with:

basket.withLock { $0.insert(element) }
3 Likes