【问题标题】:Is reading a 64-bit atomic value safe on 64-bit platforms if I write/swap using OS atomic functions with barrier?如果我使用带屏障的 OS 原子函数写入/交换,在 64 位平台上读取 64 位原子值是否安全?
【发布时间】:2019-11-01 16:50:05
【问题描述】:

问题是关于最新的 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 指令的语言编写。

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

编辑 3 谨防在 GitHub 和 SO 上传播的错误实现:也应该以安全的方式读取值(请参阅下面 Rob 的回答)

【问题讨论】:

  • 这和 C 有什么关系?
  • @Shawn 同样可以在相同平台上用 C 编写,没关系。这个问题并不特定于 Swift,而是特定于操作系统和硬件。
  • 如果这不是特定于语言的,请不要添加语言标签。
  • @Sulthan 我不同意,标有“C”的问题可以吸引对该主题有知识的低级工程师。我现在应该用 C 重写上面的代码吗?
  • 无法保证这段 Swift 代码的行为与“相似”的 C 代码相同(在重要的情况下,它不会,这是错误和混乱的常见来源)。不要假设这里的任何答案都适用于两种语言。这个问题与语言无关。

标签: ios swift macos atomic compare-and-swap


【解决方案1】:

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

'OSAtomicCompareAndSwap64Barrier' 已弃用:首先在 iOS 10 中弃用 - 改用 atomic_compare_exchange_strong()

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

How do I atomically increment a variable in 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)
        }
    }
}

通过这个小扩展(受 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 闭包中,如前面的示例所示,并实现所需的行为。

顺便说一句,请参阅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,那么实现可能看起来像这样。

【讨论】:

  • 嗨 Rob,我检查了你对我的解决方案的反对意见,我决定删除我的答案,因为你确实是对的。
  • 谢谢罗伯!不过,您的回答并没有解决这个问题:reading 是安全的,前提是它被安全地写入并使用 membarriers 并且它在内存中对齐? 32 位值也一样?内存总线仍然是 32 位 afaik,因此读取 64 位值意味着两步操作,我不确定在没有锁定的情况下读取是否安全。对于 32 位也是如此,答案更有可能是“是的,你可以”,但仍然如此。正在寻找权威的答案。
  • @jvarela 只是好奇,你的答案是什么?因为 Rob 仍然没有解决问题(是否可以在不加锁的情况下读取 64 位值)
  • @mojuba - 恕我直言,在没有同步的情况下依赖硬件功能来检索其他原子属性的值似乎并不谨慎。弃用警告建议您使用stdatomic.h,如果您使用它,它确实提供了一个atomic_load 方法,该方法明确设计用于从原子中检索值。
  • “正在读取 64 位安全值”否。 “因此读取 64 位值意味着两步操作”在某些硬件上这是正确的。在所有情况下,都不能保证它是安全的。 “对于 32 位,答案更有可能是“是的,你可以”,”不,那里也没有承诺。由于硬件实现,这可能是真的。但它并没有被承诺在 Swift 中是安全的(或者在 C 中;这也不是不可知论的,因为一种语言可以承诺它,但 Swift 和 C 没有)。使用具有诸如 atomic_load 之类的承诺的同步和工具。
猜你喜欢
  • 2019-03-28
  • 1970-01-01
  • 2010-09-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-27
  • 2015-02-16
相关资源
最近更新 更多