首页 > 解决方案 > 如果我使用带屏障的 OS 原子函数写入/交换,在 64 位平台上读取 64 位原子值是否安全?

问题描述

问题是关于最新的 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访问者:它安全吗?

如果没有,我应该怎么做才能以原子方式获取值?

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

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

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

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

标签: iosswiftmacosatomiccompare-and-swap

解决方案


OSAtomicAPI 已弃用。文档没有提到它,你也没有看到来自 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)
        }
    }
}

通过这个小扩展(受 ApplewithCriticalSection方法的启发)提供更简单的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 += 1synchronized闭包中,如前面的示例所示,并实现所需的行为。

顺便说一句,请参阅Use queue and semaphore for concurrency and property wrapper? 对于这种同步机制的一些其他实现,包括 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

//  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,那么实现可能看起来像这样。


推荐阅读