As variáveis ​​do Swift são atômicas?

102

Em Objective-C, você tem uma distinção entre propriedades atômicas e não atômicas:

@property (nonatomic, strong) NSObject *nonatomicObject;
@property (atomic, strong) NSObject *atomicObject;

Do meu entendimento, você pode ler e escrever propriedades definidas como atômicas de vários segmentos com segurança, enquanto escrever e acessar propriedades não atômicas ou ivars de vários segmentos ao mesmo tempo pode resultar em comportamento indefinido, incluindo erros de acesso incorreto.

Portanto, se você tiver uma variável como esta no Swift:

var object: NSObject

Posso ler e escrever nesta variável em paralelo com segurança? (Sem considerar o significado real de fazer isso).

Lassej
fonte
Acho que no futuro, talvez possamos usar @atomicou @nonatomic. ou apenas atômico por padrão. (Swift está tão incompleto, não podemos dizer muito agora)
Bryan Chen
1
IMO, eles farão tudo não atômico por padrão e provavelmente fornecerão um recurso especial para fazer coisas atômicas.
eonil
Como um aparte, atomicgeralmente não é considerado suficiente para interação thread-safe com uma propriedade, exceto para tipos de dados simples. Para objetos, geralmente sincroniza-se o acesso através de threads usando bloqueios (por exemplo, NSLockou @synchronized) ou filas GCD (por exemplo, fila serial ou fila simultânea com padrão "leitor-gravador").
Rob
@Rob, verdadeiro, embora devido à contagem de referência em Objective-C (e possivelmente em Swift), a leitura e gravação simultâneas em uma variável sem acesso atômico pode resultar em corrupção de memória. Se todas as variáveis ​​tivessem acesso atômico, a pior coisa que poderia acontecer seria uma condição de corrida "lógica", ou seja, um comportamento inesperado.
lassej
Não me interpretem mal: espero que a Apple responda / resolva a questão do comportamento atômico. É só que (a) atomicnão garante segurança de thread para objetos; e (b) se alguém usar corretamente uma das técnicas de sincronização acima mencionadas para garantir a segurança do thread (entre outras coisas, evitando leitura / gravação simultânea), a questão atômica é discutível. Mas ainda precisamos / queremos isso para tipos de dados simples, onde atomictem valor real. Boa pergunta!
Rob

Respostas:

52

É muito cedo para assumir, pois nenhuma documentação de baixo nível está disponível, mas você pode estudar desde a montagem. Hopper Disassembler é uma ótima ferramenta.

@interface ObjectiveCar : NSObject
@property (nonatomic, strong) id engine;
@property (atomic, strong) id driver;
@end

Usos objc_storeStronge objc_setProperty_atomicpara não atômico e atômico respectivamente, onde

class SwiftCar {
    var engine : AnyObject?    
    init() {
    }
}

usa swift_retainde libswift_stdlib_coree, aparentemente, não tem segurança de thread embutida.

Podemos especular que palavras-chave adicionais (semelhantes a @lazy) podem ser introduzidas mais tarde.

Atualização em 20/07/15 : de acordo com esta postagem do blog sobre singletons, o ambiente rápido pode tornar o thread de certos casos seguros para você, por exemplo:

class Car {
    static let sharedCar: Car = Car() // will be called inside of dispatch_once
}

private let sharedCar: Car2 = Car2() // same here
class Car2 {

}

Atualização 25/05/16 : fique de olho na proposta de evolução rápida https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md - parece que é vai ser possível ter o @atomiccomportamento implementado por você mesmo.

Sash Zats
fonte
Atualizei minha resposta com algumas informações recentes, espero que ajude
Sash Zats de
1
Ei, obrigado pelo link para a ferramenta Hopper Disassembler. Parece legal.
C0D3
11

O Swift não possui construções de linguagem em torno da segurança do thread. Presume-se que você usará as bibliotecas fornecidas para fazer seu próprio gerenciamento de segurança de thread. Há um grande número de opções que você tem na implementação de segurança de thread, incluindo pthread mutexes, NSLock e dispatch_sync como um mecanismo mutex. Veja a postagem recente de Mike Ash sobre o assunto: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html Portanto, a resposta direta à sua pergunta "Pode Leio e escrevo nesta variável em paralelo com segurança? " é não.

Bom doug
fonte
7

Provavelmente é muito cedo para responder a essa pergunta. Atualmente o swift não tem modificadores de acesso, então não há uma maneira óbvia de adicionar código que gerencia a simultaneidade em torno de um getter / setter de propriedades. Além disso, a linguagem Swift não parece ter nenhuma informação sobre concorrência ainda! (Ele também não tem KVO etc ...)

Acho que a resposta a essa pergunta ficará clara em versões futuras.

ColinE
fonte
re: falta de KVO, confira willSet, didSet- parece ser o primeiro passo no caminho
Sash Zats
1
willSet, didSet é mais para propriedades que sempre precisaram de um configurador personalizado porque tinham que fazer algo. Por exemplo, uma propriedade de cor que precisa redesenhar uma visualização quando a propriedade é alterada para um valor diferente; isso agora é feito mais facilmente usando didSet.
gnasher729
sim, é isso que quero dizer com "um primeiro passo" :) Presumi que poderia ser um sinal de que o recurso estava disponível, mas ainda não foi totalmente implementado
Sash Zats
6

Detalhes

  • Xcode 9.1, Swift 4
  • Xcode 10.2.1 (10E1001), Swift 5

Links

Tipos implementados

Ideia principal

class Example {

    private lazy var semaphore = DispatchSemaphore(value: 1)

    func executeThreadSafeFunc1() {
        // Lock access. Only first thread can execute code below.
        // Other threads will wait until semaphore.signal() will execute
        semaphore.wait()
        // your code
        semaphore.signal()         // Unlock access
    }

    func executeThreadSafeFunc2() {
        // Lock access. Only first thread can execute code below.
        // Other threads will wait until semaphore.signal() will execute
        semaphore.wait()
        DispatchQueue.global(qos: .background).async {
            // your code
            self.semaphore.signal()         // Unlock access
        }
    }
}

Amostra de acesso atômico

class Atomic {

    let dispatchGroup = DispatchGroup()
    private var variable = 0

    // Usage of semaphores

    func semaphoreSample() {

        // value: 1 - number of threads that have simultaneous access to the variable
        let atomicSemaphore = DispatchSemaphore(value: 1)
        variable = 0

        runInSeveralQueues { dispatchQueue  in
            // Only (value) queqes can run operations betwen atomicSemaphore.wait() and atomicSemaphore.signal()
            // Others queues await their turn
            atomicSemaphore.wait()            // Lock access until atomicSemaphore.signal()
            self.variable += 1
            print("\(dispatchQueue), value: \(self.variable)")
            atomicSemaphore.signal()          // Unlock access
        }

        notifyWhenDone {
            atomicSemaphore.wait()           // Lock access until atomicSemaphore.signal()
            print("variable = \(self.variable)")
            atomicSemaphore.signal()         // Unlock access
        }
    }

    // Usage of sync of DispatchQueue

    func dispatchQueueSync() {
        let atomicQueue = DispatchQueue(label: "dispatchQueueSync")
        variable = 0

        runInSeveralQueues { dispatchQueue  in

            // Only queqe can run this closure (atomicQueue.sync {...})
            // Others queues await their turn
            atomicQueue.sync {
                self.variable += 1
                print("\(dispatchQueue), value: \(self.variable)")
            }
        }

        notifyWhenDone {
            atomicQueue.sync {
                print("variable = \(self.variable)")
            }
        }
    }

    // Usage of objc_sync_enter/objc_sync_exit

    func objcSync() {
        variable = 0

        runInSeveralQueues { dispatchQueue  in

            // Only one queqe can run operations betwen objc_sync_enter(self) and objc_sync_exit(self)
            // Others queues await their turn
            objc_sync_enter(self)                   // Lock access until objc_sync_exit(self).
            self.variable += 1
            print("\(dispatchQueue), value: \(self.variable)")
            objc_sync_exit(self)                    // Unlock access
        }

        notifyWhenDone {
            objc_sync_enter(self)                   // Lock access until objc_sync_exit(self)
            print("variable = \(self.variable)")
            objc_sync_exit(self)                    // Unlock access
        }
    }
}

// Helpers

extension Atomic {

    fileprivate func notifyWhenDone(closure: @escaping ()->()) {
        dispatchGroup.notify(queue: .global(qos: .utility)) {
            closure()
            print("All work done")
        }
    }

    fileprivate func runInSeveralQueues(closure: @escaping (DispatchQueue)->()) {

        async(dispatch: .main, closure: closure)
        async(dispatch: .global(qos: .userInitiated), closure: closure)
        async(dispatch: .global(qos: .utility), closure: closure)
        async(dispatch: .global(qos: .default), closure: closure)
        async(dispatch: .global(qos: .userInteractive), closure: closure)
    }

    private func async(dispatch: DispatchQueue, closure: @escaping (DispatchQueue)->()) {

        for _ in 0 ..< 100 {
            dispatchGroup.enter()
            dispatch.async {
                let usec = Int(arc4random()) % 100_000
                usleep(useconds_t(usec))
                closure(dispatch)
                self.dispatchGroup.leave()
            }
        }
    }
}

Uso

Atomic().semaphoreSample()
//Atomic().dispatchQueueSync()
//Atomic().objcSync()

Resultado

insira a descrição da imagem aqui

Vasily Bodnarchuk
fonte
Um projeto de amostra no github seria bom!
Klaas
1
Olá! Esta é uma amostra completa. Copie a Atomicclasse e execute-a usandoAtomic().semaphoreSample()
Vasily Bodnarchuk
Sim, já fiz. Achei que seria bom tê-lo como um projeto que fosse atualizado para a sintaxe mais atual. Com o Swift, a sintaxe muda o tempo todo. E sua resposta é de longe a mais recente :)
Klaas
1

A partir do Swift 5.1, você pode usar wrappers de propriedade para criar uma lógica específica para suas propriedades. Esta é uma implementação de wrapper atômico:

@propertyWrapper
struct atomic<T> {
    private var value: T
    private let lock = NSLock()

    init(wrappedValue value: T) {
        self.value = value
    }

    var wrappedValue: T {
      get { getValue() }
      set { setValue(newValue: newValue) }
    }

    func getValue() -> T {
        lock.lock()
        defer { lock.unlock() }

        return value
    }

    mutating func setValue(newValue: T) {
        lock.lock()
        defer { lock.unlock() }

        value = newValue
    }
}

Como usar:

class Shared {
    @atomic var value: Int
...
}
iUrii
fonte
0

Aqui está o wrapper de propriedade atômica que uso extensivamente. Fiz o mecanismo de bloqueio real um protocolo, para que eu pudesse experimentar com diferentes mecanismos. Tentei semáforos DispatchQueuese o pthread_rwlock_t. O pthread_rwlock_tfoi escolhido porque parece ter a sobrecarga mais baixa e uma chance menor de uma inversão de prioridade.

/// Defines a basic signature that all locks will conform to. Provides the basis for atomic access to stuff.
protocol Lock {
    init()
    /// Lock a resource for writing. So only one thing can write, and nothing else can read or write.
    func writeLock()
    /// Lock a resource for reading. Other things can also lock for reading at the same time, but nothing else can write at that time.
    func readLock()
    /// Unlock a resource
    func unlock()
}

final class PThreadRWLock: Lock {
    private var rwLock = pthread_rwlock_t()

    init() {
        guard pthread_rwlock_init(&rwLock, nil) == 0 else {
            preconditionFailure("Unable to initialize the lock")
        }
    }

    deinit {
        pthread_rwlock_destroy(&rwLock)
    }

    func writeLock() {
        pthread_rwlock_wrlock(&rwLock)
    }

    func readLock() {
        pthread_rwlock_rdlock(&rwLock)
    }

    func unlock() {
        pthread_rwlock_unlock(&rwLock)
    }
}

/// A property wrapper that ensures atomic access to a value. IE only one thing can write at a time.
/// Multiple things can potentially read at the same time, just not during a write.
/// By using `pthread` to do the locking, this safer then using a `DispatchQueue/barrier` as there isn't a chance
/// of priority inversion.
@propertyWrapper
public final class Atomic<Value> {

    private var value: Value
    private let lock: Lock = PThreadRWLock()

    public init(wrappedValue value: Value) {
        self.value = value
    }

    public var wrappedValue: Value {
        get {
            self.lock.readLock()
            defer { self.lock.unlock() }
            return self.value
        }
        set {
            self.lock.writeLock()
            self.value = newValue
            self.lock.unlock()
        }
    }

    /// Provides a closure that will be called synchronously. This closure will be passed in the current value
    /// and it is free to modify it. Any modifications will be saved back to the original value.
    /// No other reads/writes will be allowed between when the closure is called and it returns.
    public func mutate(_ closure: (inout Value) -> Void) {
        self.lock.writeLock()
        closure(&value)
        self.lock.unlock()
    }
}
Jamone
fonte