Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is reading a 64-bit atomic value safe on 64-bit platforms if I write/swap using OS atomic functions with barrier?

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)

like image 494
mojuba Avatar asked Nov 01 '19 16:11

mojuba


1 Answers

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.

like image 158
Rob Avatar answered Sep 30 '22 18:09

Rob