Безопасно ли чтение 64-битных атомарных значений на 64-битных платформах, если я пишу / меняю, используя атомарные функции ОС с барьером? - PullRequest
6 голосов
/ 01 ноября 2019

Вопрос о последних версиях iOS и macOS. Предположим, у меня есть следующая реализация для атомарного Int64 в 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 }
}

Обратите внимание на аксессор value: это безопасно?

Если нет, что я должен сделать, чтобы получить значениеатомарно?

Кроме того, безопасна ли 32-битная версия того же класса?

Редактировать Обратите внимание, что вопрос не зависит от языка. Выше можно было бы написать на любом языке, который генерирует инструкции процессора.

Edit 2 Интерфейс OSAtomic устарел, но я думаю, что любая замена будет иметь более или менее ту же функциональность и такую ​​жезакулисное поведение. Таким образом, вопрос о том, можно ли считывать 32-битные и 64-битные значения, все еще остается на месте.

Редактировать 3 Остерегайтесь неправильных реализаций, которые распространяются на GitHub и здесь на SO, тоже: чтение значения также должно быть безопасно (см. ответ Роба ниже)

1 Ответ

7 голосов
/ 05 ноября 2019

Этот OSAtomic API устарел. Документация не упоминает об этом, и вы не видите предупреждение от Swift, но при использовании из Objective-C вы получите предупреждения об устаревании:

«OSAtomicCompareAndSwap64Barrier» устарел: сначала устарел в iOS 10- Вместо at

используйте atomic_compare_exchange_strong () (при работе в macOS предупреждает, что в macOS 10.12 он устарел).

См. Как атомарно увеличить aпеременная в 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)
        }
    }
}

С этим небольшим расширением (вдохновленным методом withCriticalSection от Apple) для обеспечения более простого 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, семафоры и т. д., а также модульный тест, который не только тестирует их, но и иллюстрирует, что простые атомарные методы доступане поддерживают потокобезопасность.


Если вы действительно хотите использовать 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, это то, чтоn реализация может выглядеть так.

...