如果我 write/swap 使用带屏障的 OS 原子函数,在 64 位平台上读取 64 位原子值是否安全?

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

问题是关于最新的 iOS 和 macOS。假设我在 Swift:

中有以下原子 Int64 的实现
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 }
}

注意 value 访问器:它安全吗?

如果不是,我应该怎么做才能自动获取值?

此外,同一个 class 的 32 位版本是否安全?

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

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

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

OSAtomic API 已弃用。文档没有提到它,你也看不到来自 Swift 的警告,但是从 Objective-C 使用你会收到弃用警告:

'OSAtomicCompareAndSwap64Barrier' is deprecated: first deprecated in iOS 10 - Use atomic_compare_exchange_strong() from instead

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

参见


您问的是:

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.

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


就我个人而言,我建议您不要使用 OSAtomic。从 Objective-C 你可以考虑使用 stdatomic.h,但是从 Swift 我建议使用一种标准的通用同步机制,例如 GCD 串行队列,GCD reader-writer 模式,或基于 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)
        }
    }
}

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

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

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

var foo = Synchronized<Int>(0)

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

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

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

请注意,虽然我为 value 提供了同步访问器方法,但这仅适用于简单的加载和存储。我在这里不使用它,因为我们希望将整个加载、增量和存储同步为一个任务。所以我使用 synchronized 方法。考虑以下因素:

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

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

它看起来很合理,因为它使用同步 value 访问器。但它只是行不通,因为同步逻辑处于错误的级别。我们不需要单独同步这个值的加载、增量和存储,我们真的需要将所有三个步骤一起同步。因此,我们将整个 value += 1 包裹在 synchronized 闭包中,如上例所示,并实现所需的行为。

顺便说一句,参见 了解这种同步机制的一些其他实现,包括 GCD 串行队列、GCD reader-writer、信号量等,以及单元测试这不仅对这些进行了基准测试,而且还说明了简单的原子访问器方法不是线程安全的。


如果你真的想使用 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

//  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

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

let object = AtomicInt()

object.value = 0

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

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

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

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