7

The question is about the latest iOS and macOS. Suppose I have the following implementation for an atomic Int64 in Swift:

struct AtomicInt64 {

    private var _value: Int64 = 0

    init(_ value: Int64) {
        set(value)
    }

    mutating func set(_ newValue: Int64) {
        while !OSAtomicCompareAndSwap64Barrier(_value, newValue, &_value) { }
    }

    mutating func setIf(expectedValue: Int64, _ newValue: Int64) -> Bool {
        return OSAtomicCompareAndSwap64Barrier(expectedValue, newValue, &_value)
    }

    var value: Int64 { _value }
}

Note the value accessor: is it safe?

If not, what should I do to fetch the value atomically?

Also, would the 32-bit version of the same class be safe?

Edit please note that the question is language agnostic. The above could have been written in any language that generates CPU instructions.

Edit 2 The OSAtomic interface is deprecated now but I think any replacement would have more or less the same functionality and the same behaviour behind the scenes. So the question of whether 32-bit and 64-bit values can be read safely is still in place.

Edit 3 BEWARE of incorrect implementations that circulate on GitHub and here on SO, too: reading the value should also be made in a safe manner (see Rob's answer below)

curiousguy
  • 8,038
  • 2
  • 40
  • 58
mojuba
  • 11,842
  • 9
  • 51
  • 72
  • 4
    What does this have to do with C? – Shawn Nov 01 '19 at 17:12
  • @Shawn same could be written in C on the same platforms, it doesn't matter. The question is not specific to Swift, it's specific to OS and hardware. – mojuba Nov 01 '19 at 18:09
  • 2
    If this is not specific to a language, don't add language tags, please. – Sulthan Nov 01 '19 at 18:19
  • @Sulthan I disagree, the question tagged with "C" can attract lower-level engineers who are knowledgeable on the subject. Should I rewrite the above code in C now? – mojuba Nov 01 '19 at 20:52
  • 2
    There is no promise that this Swift code will behave identically to "similar" C code (and in important cases, it will not, and this is a common source of bugs and confusion). Do not assume that any answer here would apply to both languages. This question is not language agnostic. – Rob Napier Nov 01 '19 at 22:02
  • @RobNapier I'm pretty confident the code in the question can be rewritten in C to produce identical binary. There may be minor differences not relevant to the issue of atomicity. The question is about whether a 64 bit value that's written using OSAtomicCompareAndSwap64Barrier() can be safely read. I might just as well remove the code from the question but I don't want to. – mojuba Nov 01 '19 at 22:32

1 Answers1

7

This OSAtomic API is deprecated. The documentation doesn’t mention it and you don’t see the warning from Swift, but used from Objective-C you will receive deprecation warnings:

'OSAtomicCompareAndSwap64Barrier' is deprecated: first deprecated in iOS 10 - Use atomic_compare_exchange_strong() from <stdatomic.h> instead

(If working on macOS, it warns you that it was deprecated in macOS 10.12.)

See How do I atomically increment a variable in Swift?


You asked:

The OSAtomic interface is deprecated now but I think any replacement would have more or less the same functionality and the same behaviour behind the scenes. So the question of whether 32-bit and 64-bit values can be read safely is still in place.

The suggested replacement is stdatomic.h. It has a atomic_load method, and I would use that rather than accessing directly.


Personally, I’d suggest you don’t use OSAtomic. From Objective-C you could consider using stdatomic.h, but from Swift I’d advise using one of the standard common synchronization mechanisms such as GCD serial queues, GCD reader-writer pattern, or NSLock based approaches. Conventional wisdom is that GCD was faster than locks, but all my recent benchmarks seem to suggest that the opposite is true now.

So I might suggest using locks:

class Synchronized<Value> {
    private var _value: Value
    private var lock = NSLock()

    init(_ value: Value) {
        self._value = value
    }

    var value: Value {
        get { lock.synchronized { _value } }
        set { lock.synchronized { _value = newValue } }
    }

    func synchronized<T>(block: (inout Value) throws -> T) rethrows -> T {
        try lock.synchronized {
            try block(&_value)
        }
    }
}
    

With this little extension (inspired by Apple’s withCriticalSection method) to provide simpler NSLock interaction:

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

Then, I can declare a synchronized integer:

let foo = Synchronized<Int>(0)

And now I can increment that a million times from multiple threads like so:

DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
    foo.synchronized { value in
        value += 1
    }
}

print(foo.value)    // 1,000,000

Note, while I provide synchronized accessor methods for value, that’s only for simple loads and stores. I’m not using it here because we want the entire load, increment, and store to be synchronized as a single task. So I’m using the synchronized method. Consider the following:

DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
    foo.value += 1
}

print(foo.value)    // not 1,000,000 !!!

It looks reasonable because it’s using synchronized value accessors. But it just doesn’t work because the synchronization logic is at the wrong level. Rather than synchronizing the load, the increment, and the store of this value individually, we really need all three steps to be synchronized all together. So we wrap the whole value += 1 within the synchronized closure, like shown in the previous example, and achieve the desired behavior.

By the way, see Use queue and semaphore for concurrency and property wrapper? for a few other implementations of this sort of synchronization mechanism, including GCD serial queues, GCD reader-writer, semaphores, etc., and a unit test that not only benchmarks these, but also illustrates that simple atomic accessor methods are not thread-safe.


If you really wanted to use stdatomic.h, you can implement that in Objective-C:

//  Atomic.h

@import Foundation;

NS_ASSUME_NONNULL_BEGIN

@interface AtomicInt: NSObject

@property (nonatomic) int value;

- (void)add:(int)value;

@end

NS_ASSUME_NONNULL_END

And

//  AtomicInt.m

#import "AtomicInt.h"
#import <stdatomic.h>

@interface AtomicInt()
{
    atomic_int _value;
}
@end

@implementation AtomicInt

// getter

- (int)value {
    return atomic_load(&_value);
}

// setter

- (void)setValue:(int)value {
    atomic_store(&_value, value);
}

// add methods for whatever atomic operations you need

- (void)add:(int)value {
    atomic_fetch_add(&_value, value);
}

@end

Then, in Swift, you can do things like:

let object = AtomicInt()

object.value = 0

DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
    object.add(1)
}

print(object.value)    // 1,000,000

Clearly, you would add whatever atomic operations you need to your Objective-C code (I only implemented atomic_fetch_add, but hopefully it illustrates the idea).

Personally, I’d stick with more conventional Swift patterns, but if you really wanted to use the suggested replacement for OSAtomic, this is what an implementation could possibly look like.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    Hi Rob, I checked your objections to my solution and I decided to delete my answer, because you were indeed right. – jvarela Nov 05 '19 at 13:45
  • Thanks Rob! Your answer doesn't address the question though: is *reading* a 64-bit value safe, provided that it's being written to safely and with membarriers and that it is aligned in memory? Same for 32-bit values? Memory buses are still 32-bit afaik, so reading a 64-bit value means a two-step operation and I'm not sure if it's safe to read without locking. And the same for 32-bit for which the answer is more likely "yes, you can", but still. Was looking for an authoritative answer. – mojuba Nov 05 '19 at 15:43
  • @jvarela just curious, what was your answer? Because Rob's still didn't address the question (whether 64-bit values can be read without locking) – mojuba Nov 05 '19 at 16:02
  • His answer was a similar permutation of “don’t use `OSAtomic`”, but he instead suggested using GCD serial queue with synchronization just at the `value` accessor properties. – Rob Nov 05 '19 at 16:05
  • 3
    @mojuba - IMHO, it does not seem prudent to rely on hardware features to just retrieve values for your otherwise atomic property without some synchronization. The deprecation warning suggests you use `stdatomic.h`, and if you use that, it does provide a `atomic_load` method which is designed explicitly for retrieving the value from the atomic. – Rob Nov 05 '19 at 16:27
  • 3
    "is reading a 64-bit value safe" no. "so reading a 64-bit value means a two-step operation" On some hardware this is true. In all cases, it is not promised to be safe. "for 32-bit for which the answer is more likely "yes, you can"," No, it's not promised there either. It might be true due to hardware implementations. But it's not promised to be safe in Swift (or in C; again this isn't agnostic because a language *could* promise it, but Swift and C don't). Use synchronization and tools with promises like `atomic_load`. – Rob Napier Nov 05 '19 at 17:45
  • Thanks a lot for a detailed answer, Rob! – mojuba Nov 06 '19 at 15:43