Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

is reference assignment atomic in Swift 5?

Do I need some kind of explicit synchronization in this case?

class A { 
  let val: Int; 
  init(_ newVal: Int) { 
    val = newVal
   }
}

public class B {
  var a: A? = nil
  public func setA() { a = A(0) }
  public func hasA() -> Bool { return a != nil }
}

There is also another method in class B:

public func resetA() {
  guard hasA() else { return }
  a = A(1)
}

setA() and resetA() may be called from any thread, in any order.

I understand that there may be a race condition, that if concurrently one thread calls setA() and another thread calls resetA(), the result is not determined: val will either be 0, or 1, but I don't care: at any rate, hasA() will return true, won't it?

Does the answer change if A is a struct instead of class?

like image 244
Alex Cohn Avatar asked Jan 21 '21 15:01

Alex Cohn


People also ask

Are assignment operations atomic?

Yes. Assignment of premitive types and reference types are atomic. Read the ECMA C# language specification.

Are Swift properties Atomic?

Atomic property is one of the commonly-requested Swift features, that exists in Objective-C. By default, an Objective-C property is atomic. Defining a property as atomic should guarantee that it can be safely read and written from different threads.

Is Java reference assignment Atomic?

First of all, reference assignment is atomic because the specification says so. Besides that, there is no obstacle for JVM implementors to fulfill this constraint, as 64 Bit references are usually only used on 64 Bit architectures, where atomic 64 Bit assignment comes for free.

Is reference assignment atomic in c#?

Reference assignment is atomic. Interlocked. Exchange does not do only reference assignment. It does a read of the current value of a variable, stashes away the old value, and assigns the new value to the variable, all as an atomic operation.


1 Answers

In short, no, property accessors are not atomic. See WWDC 2016 video Concurrent Programming With GCD in Swift 3, which talks about the absence of atomic/synchronization native in the language. (This is a GCD talk, so when they subsequently dive into synchronization methods, they focus on GCD methods, but any synchronization method is fine.) Apple uses a variety of different synchronization methods in their own code. E.g. in ThreadSafeArrayStore they use they use NSLock).


If synchronizing with locks, I might suggest an extension like the following:

extension NSLocking {
    func synchronized<T>(block: () throws -> T) rethrows -> T {
        lock()
        defer { unlock() }
        return try block()
    }
}

Apple uses this pattern in their own code, though they happen to call it withLock rather than synchronized. But the pattern is the same.

Then you can do:

public class B {
    private var lock = NSLock()
    private var a: A?             // make this private to prevent unsynchronized direct access to this property

    public func setA() {
        lock.synchronized {
            a = A(0)
        }
    }

    public func hasA() -> Bool {
        lock.synchronized {
            a != nil
        }
    }

    public func resetA() {
        lock.synchronized {
            guard a != nil else { return }
            a = A(1)
        }
    }
}

Or perhaps

public class B {
    private var lock = NSLock()
    private var _a: A?

    public var a: A? {
        get { lock.synchronized { _a } }
        set { lock.synchronized { _a = newValue } }
    }

    public var hasA: Bool {
        lock.synchronized { _a != nil }
    }

    public func resetA() {
        lock.synchronized {
            guard _a != nil else { return }
            _a = A(1)
        }
    }
}

I confess to some uneasiness in exposing hasA, because it practically invites the application developer to write like:

if !b.hasA {
    b.a = ...
}

That is fine in terms of preventing simultaneous access to memory, but it introduced a logical race if two threads are doing it at the same time, where both happen to pass the !hasA test, and they both replace the value, the last one wins.

Instead, I might write a method to do this for us:

public class B {
    private var lock = NSLock() // replacing os_unfair_lock_s()
    private var _a: A? = nil // fixed, thanks to Rob

    var a: A? {
        get { lock.synchronized { _a } }
        set { lock.synchronized { _a = newValue } }
    }

    public func withA(block: (inout A?) throws -> T) rethrows -> T {
        try lock.synchronized {
            try block(&_a)
        }
    }
}

That way you can do:

b.withA { a in
    if a == nil {
        a = ...
    }
}

That is thread safe, because we are letting the caller wrap all the logical tasks (checking to see if a is nil and, if so, the initialization of a) all in one single synchronized step. It is a nice generalized solution to the problem. And it prevents logic races.


Now the above example is so abstract that it is hard to follow. So let's consider a practical example, a variation on Apple’s ThreadSafeArrayStore:

public class ThreadSafeArrayStore<Value> {
    private var underlying: [Value]
    private let lock = NSLock()

    public init(_ seed: [Value] = []) {
        underlying = seed
    }

    public subscript(index: Int) -> Value {
        get { lock.synchronized { underlying[index] } }
        set { lock.synchronized { underlying[index] = newValue } }
    }

    public func get() -> [Value] {
        lock.synchronized {
            underlying
        }
    }

    public func clear() {
        lock.synchronized {
            underlying = []
        }
    }

    public func append(_ item: Value) {
        lock.synchronized {
            underlying.append(item)
        }
    }

    public var count: Int {
        lock.synchronized {
            underlying.count
        }
    }

    public var isEmpty: Bool {
        lock.synchronized {
            underlying.isEmpty
        }
    }

    public func map<NewValue>(_ transform: (Value) throws -> NewValue) rethrows -> [NewValue] {
        try lock.synchronized {
            try underlying.map(transform)
        }
    }

    public func compactMap<NewValue>(_ transform: (Value) throws -> NewValue?) rethrows -> [NewValue] {
        try lock.synchronized {
            try underlying.compactMap(transform)
        }
    }
}

Here we have a synchronized array, where we define an interface to interact with the underlying array in a thread-safe manner.


Or, if you want an even more trivial example, consider an thread-safe object to keep track of what the tallest item was. We would not have a hasValue boolean, but rather we would incorporate that right into our synchronized updateIfTaller method:

public class Tallest {
    private var _height: Float?
    private let lock = NSLock()

    var height: Float? {
        lock.synchronized { _height }
    }

    func updateIfTaller(_ candidate: Float) {
        lock.synchronized {
            guard let tallest = _height else {
                _height = candidate
                return
            }

            if candidate > tallest {
                _height = candidate
            }
        }
    }
}

Just a few examples. Hopefully it illustrates the idea.

like image 112
Rob Avatar answered Sep 21 '22 11:09

Rob