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

moj*_*uba 6 macos atomic compare-and-swap ios swift

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 }
}
Run Code Online (Sandbox Code Playgroud)

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?

编辑请注意,该问题与语言无关。上面的代码可以用任何生成CPU指令的语言编写。

编辑2现在已弃用OSAtomic界面,但我认为任何替换都将在幕后或多或少具有相同的功能和相同的行为。因此,是否可以安全地读取32位和64位值的问题仍然存在。

编辑3注意在GitHub上以及在SO上流传的不正确的实现:也应该以安全的方式读取值(请参见下面的Rob的回答)

Rob*_*Rob 7

OSAtomic不推荐使用此API。文档没有提及它,您也看不到Swift发出的警告,但是从Objective-C使用时,您将收到弃用警告:

不推荐使用'OSAtomicCompareAndSwap64Barrier':在iOS 10中首次不推荐使用-改为使用atomic_compare_exchange_strong()

(如果在macOS上运行,它会警告您在macOS 10.12中已弃用它。)

请参阅如何在Swift中自动递增变量?


您问:

现在已弃用OSAtomic接口,但我认为任何替换都将在幕后或多或少具有相同的功能和相同的行为。因此,是否可以安全地读取32位和64位值的问题仍然存在。

建议替换为stdatomic.h。它有一个atomic_load方法,我会用它而不是直接访问。


就个人而言,我建议您不要使用OSAtomic。在Objective-C中,您可以考虑使用stdatomic.h,但是在Swift中,我建议您使用一种标准的通用同步机制,例如GCD串行队列,GCD读写器模式或NSLock基于方法。传统观点认为,GCD比锁快,但是我最近的所有基准测试似乎都表明相反。

所以我可能建议使用锁:

struct 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 } }
    }

    mutating func synchronized<T>(block: (inout Value) throws -> T) rethrows -> T {
        return try lock.synchronized {
            try block(&_value)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

通过这个小扩展(受Apple withCriticalSection方法启发),可以提供更简单的NSLock交互:

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

然后,我可以声明一个同步整数:

var foo = Synchronized<Int>(0)
Run Code Online (Sandbox Code Playgroud)

现在,我可以像这样从多个线程中增加一百万次:

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

print(foo.value)    // 1,000,000
Run Code Online (Sandbox Code Playgroud)

请注意,尽管我为提供了同步的访问器方法value,但这仅用于简单的加载和存储。我在这里不使用它,因为我们希望将整个负载,增量和存储作为单个任务进行同步。所以我正在使用该synchronized方法。考虑以下:

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

print(foo.value)    // not 1,000,000 !!!
Run Code Online (Sandbox Code Playgroud)

看起来很合理,因为它使用了同步value访问器。但这只是不起作用,因为同步逻辑处于错误的级别。与其单独同步该值的负载,增量和存储,我们实际上需要将所有三个步骤都同步在一起。因此,就像前面的示例中所示,我们将整体包装value += 1synchronized闭包中,并实现了所需的行为。

顺便说一句,请参阅将队列和信号量用于并发和属性包装器?对于这种同步机制的其他一些实现,包括GCD串行队列,GCD读写器,信号量等,以及一个单元测试,它不仅对这些进行基准测试,而且还说明了简单的原子访问器方法不是线程安全的。


如果您确实想使用stdatomic.h,可以在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
Run Code Online (Sandbox Code Playgroud)

//  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
Run Code Online (Sandbox Code Playgroud)

然后,在Swift中,您可以执行以下操作:

let object = AtomicInt()

object.value = 0

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

print(object.value)    // 1,000,000
Run Code Online (Sandbox Code Playgroud)

显然,您可以在Objective-C代码中添加所需的任何原子操作(我只是实现了atomic_fetch_add,但希望它能说明这个想法)。

就个人而言,我会坚持使用更传统的Swift模式,但是如果您真的想使用建议的OSAtomic替代品,那么实现可能看起来像这样。

  • @mojuba-恕我直言,依靠硬件功能在没有同步的情况下仅获取原子属性的值似乎并不明智。弃用警告建议您使用`stdatomic.h`,如果使用它,它会提供`atomic_load`方法,该方法专门用于从原子中检索值。 (3认同)
  • “正在读取64位值安全”否。“因此,读取64位值意味着需要两步操作”,在某些硬件上确实如此。在所有情况下,都不保证安全。“对于答案更可能为“是的,您可以的”的32位,”,不,那里也没有承诺。由于硬件实现,这可能是正确的。但这并不能保证它在Swift中是安全的(或者在C语言中;这也不是不可知的,因为某种语言可以(可以)保证它,但是Swift和C不能)。将同步和工具与“ atomic_load”之类的承诺一起使用。 (3认同)