Qual è l'equivalente di Swift a "@synchronized" di Objective-C?


231

Ho cercato nel libro Swift, ma non riesco a trovare la versione Swift di @synchronized. Come posso fare l'esclusione reciproca in Swift?


1
Vorrei usare una barriera di spedizione. Le barriere forniscono una sincronizzazione molto economica. dispatch_barrier_async (). ecc.
Frederick C. Lee,

@ FrederickC.Lee, cosa succede se è necessario sincronizzare una scrittura , ad esempio quando si crea un wrapper removeFirst()?
ScottyBlades,

Risposte:


183

Puoi usare GCD. È un po 'più dettagliato di @synchronized, ma funziona come un sostituto:

let serialQueue = DispatchQueue(label: "com.test.mySerialQueue")
serialQueue.sync {
    // code
}

12
Questo è fantastico, ma manca l'abilità di rientro che hai con @synchronized.
Michael Waterfall,

9
Con questo approccio devi stare attento. Il tuo blocco potrebbe essere eseguito su qualche altro thread. I documenti API dicono: "Come ottimizzazione, questa funzione richiama il blocco sul thread corrente quando possibile".
bio

20
Grande articolo di Matt Gallagher su questo: cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html
wuf810,

4
No, questo provoca deadlock occasionali.
Tom Kraina,

71
No, no e no. Bel tentativo, ma funziona perfettamente. Perché? Lettura essenziale (confronto completo di alternative, precauzioni) e un ottimo framework di utilità di Matt Gallagher, qui: cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html @ wuf810 ne ha menzionato prima (HT), ma sottovalutato quanto è buono questo articolo. Tutti dovrebbero leggere. (Si prega di upvote questo il minimo di renderlo inizialmente visibile, ma non di più.)
t0rst

181

Lo stavo cercando da solo e sono giunto alla conclusione che non esiste ancora un costrutto nativo all'interno di rapido per questo.

Ho creato questa piccola funzione di supporto basata su alcuni dei codici che ho visto da Matt Bridges e altri.

func synced(_ lock: Any, closure: () -> ()) {
    objc_sync_enter(lock)
    closure()
    objc_sync_exit(lock)
}

L'utilizzo è piuttosto semplice

synced(self) {
    println("This is a synchronized closure")
}

C'è un problema che ho riscontrato con questo. Passare in un array come argomento lock sembra causare un errore del compilatore molto ottuso a questo punto. Altrimenti però sembra funzionare come desiderato.

Bitcast requires both operands to be pointer or neither
  %26 = bitcast i64 %25 to %objc_object*, !dbg !378
LLVM ERROR: Broken function found, compilation aborted!

Bello! Si prega di presentare un bug per questo se è ancora un problema in 1.0
MattD

14
Questo è abbastanza utile e preserva bene la sintassi del @synchronizedblocco, ma nota che non è identico a un'istruzione di blocco reale incorporata come il @synchronizedblocco in Objective-C, perché le istruzioni returne breaknon funzionano più per saltare fuori dalla funzione / loop circostante come sarebbe se questa fosse un'affermazione normale.
Newattt

3
L'errore è probabilmente dovuto al fatto che le matrici vengono passate come valori non come riferimenti
james_alvarez,

9
Questo sarebbe probabilmente un ottimo posto per usare la nuova deferparola chiave per assicurarsi che objc_sync_exitvenga chiamato anche se closuregenera.
devios1,

3
@ t0rst Chiamare questa risposta "imperfetta" in base all'articolo collegato non è valido. L'articolo dice che questo metodo è "un po 'più lento dell'ideale" e "è limitato alle piattaforme Apple". Ciò non lo rende "imperfetto" da un colpo lungo.
RenniePet,

151

Mi piacciono e uso molte delle risposte qui, quindi sceglierei quale funziona meglio per te. Detto questo, il metodo che preferisco quando ho bisogno di qualcosa di simile a object-c @synchronizedusa l' deferistruzione introdotta in swift 2.

{ 
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }

    //
    // code of critical section goes here
    //

} // <-- lock released when this block is exited

La cosa bella di questo metodo, è che la vostra sezione critica può uscire dal blocco che contiene in alcun modo desiderato (ad esempio, return, break, continue, throw), e "le dichiarazioni all'interno dell'istruzione Defer vengono eseguite non importa come viene trasferito il controllo del programma." 1


Penso che questa sia probabilmente la soluzione più elegante fornita qui. Grazie per il tuo feedback
Scott D,

3
Che cosa è lock? Come viene lockinizializzato?
Van Du Tran,

6
lockè un qualsiasi oggetto-c.
ɲeuroburɳ

1
Eccellente! Avevo scritto alcuni metodi per l'helper lock quando fu introdotto Swift 1 e non li rivisitavo da un po '. Dimenticato completamente il differimento; questa è la strada da percorrere!
Randy,

Mi piace questo, ma viene visualizzato un errore del compilatore "Blocco di istruzioni con parentesi è una chiusura inutilizzata" in Xcode 8. Ah capisco che sono solo le parentesi graffe della funzione - troppo tempo per trovare il link di riferimento "1" - grazie!
Duncan Groenewald,

83

È possibile inserire istruzioni tra objc_sync_enter(obj: AnyObject?)e objc_sync_exit(obj: AnyObject?). La parola chiave @synchronized sta usando quei metodi sotto le copertine. vale a dire

objc_sync_enter(self)
... synchronized code ...
objc_sync_exit(self)

3
Questo sarà considerato l'uso di un'API privata da parte di Apple?
Drux,

2
No, objc_sync_entere objc_sync_exitsono metodi definiti in Objc-sync.h e sono open source: opensource.apple.com/source/objc4/objc4-371.2/runtime/…
bontoJR

Cosa succede se più thread tentano di accedere alla stessa risorsa, il secondo attende, riprova o si arresta in modo anomalo?
TruMan1,

Aggiungendo ciò che ha detto @bontoJR, objc_sync_enter(…)e objc_sync_exit(…)sono le intestazioni pubbliche fornite da iOS / macOS / ecc. API (sembra che siano all'interno ….sdkdel percorso usr/include/objc/objc-sync.h) . Il modo più semplice per scoprire se qualcosa è un'API pubblica o no è digitare (in Xcode) il nome della funzione (ad es objc_sync_enter(). Non è necessario specificare gli argomenti per le funzioni C) , quindi provare a fare clic su di esso. Se ti mostra il file di intestazione per quell'API, allora sei bravo (dal momento che non saresti in grado di vedere l'intestazione se non fosse pubblica) .
Slipp D. Thompson,

75

L'analogo della @synchronizeddirettiva di Objective-C può avere un tipo di ritorno arbitrario e un rethrowscomportamento piacevole in Swift.

// Swift 3
func synchronized<T>(_ lock: AnyObject, _ body: () throws -> T) rethrows -> T {
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }
    return try body()
}

L'uso deferdell'istruzione consente di restituire direttamente un valore senza introdurre una variabile temporanea.


In Swift 2 aggiungi l' @noescapeattributo alla chiusura per consentire ulteriori ottimizzazioni:

// Swift 2
func synchronized<T>(lock: AnyObject, @noescape _ body: () throws -> T) rethrows -> T {
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }
    return try body()
}

Basato sulle risposte di GNewc [1] (dove mi piace il tipo di ritorno arbitrario) e Tod Cunningham [2] (dove mi piace defer).


Xcode mi sta dicendo che @noescape è ora predefinito ed è deprecato in Swift 3.
RenniePet

Esatto, il codice in questa risposta è per Swift 2 e richiede alcuni adattamenti per Swift 3. Lo aggiornerò quando avrò tempo.
Werediver,

1
Puoi spiegare l'uso? Forse con un esempio .. grazie in anticipo! Nel mio caso, ho un set che devo sincronizzare, perché ne manipolo il contenuto in DispatchQueue.
sancho,

@sancho Preferirei mantenere questo post conciso. Sembra che tu chieda delle linee guida generali sulla programmazione concorrente, questa è una domanda ampia. Prova a fare una domanda separata!
Werediver,

41

SWIFT 4

In Swift 4 è possibile utilizzare le code di invio GCD per bloccare le risorse.

class MyObject {
    private var internalState: Int = 0
    private let internalQueue: DispatchQueue = DispatchQueue(label:"LockingQueue") // Serial by default

    var state: Int {
        get {
            return internalQueue.sync { internalState }
        }

        set (newState) {
            internalQueue.sync { internalState = newState }
        }
    }
} 

Questo non sembra funzionare con XCode8.1. .serialsembra non essere disponibile. Ma .concurrentè disponibile : /
Travis Griggs,

2
il valore predefinito è .serial
Duncan Groenewald,

2
Si noti che questo modello non protegge adeguatamente dai più comuni problemi multi thread. Ad esempio, se si eseguisse myObject.state = myObject.state + 1contemporaneamente, non conterebbe le operazioni totali ma produrrebbe invece un valore non deterministico. Per risolvere questo problema, il codice chiamante dovrebbe essere racchiuso in una coda seriale in modo che sia la lettura che la scrittura avvengano atomicamente. Ovviamente Obj-c @synchronisedha lo stesso problema, quindi in questo senso la tua implementazione è corretta.
Berik,

1
Sì, myObject.state += 1è una combinazione di un'operazione di lettura e quindi di scrittura. Alcuni altri thread possono ancora trovarsi in mezzo per impostare / scrivere un valore. Secondo objc.io/blog/2018/12/18/atomic-variables , sarebbe invece più semplice eseguire setin un blocco / chiusura di sincronizzazione e non sotto la variabile stessa.
CyberMew,

24

Usando la risposta di Bryan McLemore, l'ho estesa per supportare oggetti che gettano in un maniero sicuro con l'abilità di differimento di Swift 2.0.

func synchronized( lock:AnyObject, block:() throws -> Void ) rethrows
{
    objc_sync_enter(lock)
    defer {
        objc_sync_exit(lock)
    }

    try block()
}

Sarebbe meglio usare rethrowsper semplificare l'uso con chiusure non gettanti (non c'è bisogno di usare try), come mostrato nella mia risposta .
Werediver,

23

Per aggiungere la funzionalità di ritorno, è possibile effettuare ciò:

func synchronize<T>(lockObj: AnyObject!, closure: ()->T) -> T
{
  objc_sync_enter(lockObj)
  var retVal: T = closure()
  objc_sync_exit(lockObj)
  return retVal
}

Successivamente, puoi chiamarlo usando:

func importantMethod(...) -> Bool {
  return synchronize(self) {
    if(feelLikeReturningTrue) { return true }
    // do other things
    if(feelLikeReturningTrueNow) { return true }
    // more things
    return whatIFeelLike ? true : false
  }
}

10

Swift 3

Questo codice ha la possibilità di rientrare e può funzionare con chiamate di funzione asincrone. In questo codice, dopo che viene chiamato someAsyncFunc (), verrà elaborata un'altra chiusura della funzione sulla coda seriale, ma verrà bloccata da semaphore.wait () fino alla chiamata di signal (). internalQueue.sync non dovrebbe essere usato in quanto bloccherebbe il thread principale se non sbaglio.

let internalQueue = DispatchQueue(label: "serialQueue")
let semaphore = DispatchSemaphore(value: 1)

internalQueue.async {

    self.semaphore.wait()

    // Critical section

    someAsyncFunc() {

        // Do some work here

        self.semaphore.signal()
    }
}

objc_sync_enter / objc_sync_exit non è una buona idea senza la gestione degli errori.


Quale gestione degli errori? Il compilatore non consentirà nulla di ciò che genera. D'altra parte, non usando objc_sync_enter / exit, si rinuncia ad alcuni miglioramenti sostanziali delle prestazioni.
gnasher729,

8

Nella sessione "Informazioni sugli arresti anomali e sui registri degli arresti anomali" 414 del WWDC 2018 mostrano il seguente modo usando DispatchQueues con sincronizzazione.

In swift 4 dovrebbe essere qualcosa di simile al seguente:

class ImageCache {
    private let queue = DispatchQueue(label: "sync queue")
    private var storage: [String: UIImage] = [:]
    public subscript(key: String) -> UIImage? {
        get {
          return queue.sync {
            return storage[key]
          }
        }
        set {
          queue.sync {
            storage[key] = newValue
          }
        }
    }
}

Ad ogni modo è anche possibile effettuare letture più velocemente utilizzando code simultanee con barriere. Le letture di sincronizzazione e asincrono vengono eseguite contemporaneamente e la scrittura di un nuovo valore attende il completamento delle operazioni precedenti.

class ImageCache {
    private let queue = DispatchQueue(label: "with barriers", attributes: .concurrent)
    private var storage: [String: UIImage] = [:]

    func get(_ key: String) -> UIImage? {
        return queue.sync { [weak self] in
            guard let self = self else { return nil }
            return self.storage[key]
        }
    }

    func set(_ image: UIImage, for key: String) {
        queue.async(flags: .barrier) { [weak self] in
            guard let self = self else { return }
            self.storage[key] = image
        }
    }
}

probabilmente non è necessario bloccare le letture e rallentare la coda utilizzando la sincronizzazione. Puoi semplicemente usare la sincronizzazione per la scrittura seriale.
Basheer_CAD,

6

Usa NSLock in Swift4:

let lock = NSLock()
lock.lock()
if isRunning == true {
        print("Service IS running ==> please wait")
        return
} else {
    print("Service not running")
}
isRunning = true
lock.unlock()

Avviso La classe NSLock utilizza thread POSIX per implementare il suo comportamento di blocco. Quando si invia un messaggio di sblocco a un oggetto NSLock, è necessario assicurarsi che il messaggio venga inviato dallo stesso thread che ha inviato il messaggio di blocco iniziale. Lo sblocco di un blocco da un thread diverso può comportare un comportamento indefinito.



6

Nella moderna Swift 5, con funzionalità di ritorno:

/**
Makes sure no other thread reenters the closure before the one running has not returned
*/
@discardableResult
public func synchronized<T>(_ lock: AnyObject, closure:() -> T) -> T {
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }

    return closure()
}

Usalo in questo modo, per sfruttare la capacità del valore di ritorno:

let returnedValue = synchronized(self) { 
     // Your code here
     return yourCode()
}

O così altrimenti:

synchronized(self) { 
     // Your code here
    yourCode()
}

2
Questa è la risposta corretta e non quella accettata e altamente votata (che dipende da GCD). Sembra che praticamente nessuno usi o capisca come usare Thread. Ne sono contento, mentre GCDè pieno di problemi e limitazioni.
javadba,

4

Prova: NSRecursiveLock

Un blocco che può essere acquisito più volte dallo stesso thread senza causare un deadlock.

let lock = NSRecursiveLock()

func f() {
    lock.lock()
    //Your Code
    lock.unlock()
}

func f2() {
    lock.lock()
    defer {
        lock.unlock()
    }
    //Your Code
}

2

Figura posterò la mia implementazione di Swift 5, basata sulle risposte precedenti. Grazie ragazzi! Ho trovato utile averne uno che restituisca anche un valore, quindi ho due metodi.

Ecco una semplice classe da fare per prima:

import Foundation
class Sync {
public class func synced(_ lock: Any, closure: () -> ()) {
        objc_sync_enter(lock)
        defer { objc_sync_exit(lock) }
        closure()
    }
    public class func syncedReturn(_ lock: Any, closure: () -> (Any?)) -> Any? {
        objc_sync_enter(lock)
        defer { objc_sync_exit(lock) }
        return closure()
    }
}

Quindi usalo in questo modo se hai bisogno di un valore di ritorno:

return Sync.syncedReturn(self, closure: {
    // some code here
    return "hello world"
})

O:

Sync.synced(self, closure: {
    // do some work synchronously
})

Prova public class func synced<T>(_ lock: Any, closure: () -> T), funziona sia per il vuoto che per qualsiasi altro tipo. C'è anche roba da ricrescita.
hnh

@hnh cosa intendi per roba ricrescita? Inoltre, se vorresti condividere una chiamata di esempio con il metodo generico con il tipo <T> che mi aiuterebbe ad aggiornare la risposta, mi piace dove stai andando.
TheJeff

genera nuovamente, non ricresce, SRZ
hnh

1

Dettagli

Codice x 8.3.1, rapido 3.1

Compito

Leggi il valore di scrittura da diversi thread (asincrono).

Codice

class AsyncObject<T>:CustomStringConvertible {
    private var _value: T
    public private(set) var dispatchQueueName: String

    let dispatchQueue: DispatchQueue

    init (value: T, dispatchQueueName: String) {
        _value = value
        self.dispatchQueueName = dispatchQueueName
        dispatchQueue = DispatchQueue(label: dispatchQueueName)
    }

    func setValue(with closure: @escaping (_ currentValue: T)->(T) ) {
        dispatchQueue.sync { [weak self] in
            if let _self = self {
                _self._value = closure(_self._value)
            }
        }
    }

    func getValue(with closure: @escaping (_ currentValue: T)->() ) {
        dispatchQueue.sync { [weak self] in
            if let _self = self {
                closure(_self._value)
            }
        }
    }


    var value: T {
        get {
            return dispatchQueue.sync { _value }
        }

        set (newValue) {
            dispatchQueue.sync { _value = newValue }
        }
    }

    var description: String {
        return "\(_value)"
    }
}

uso

print("Single read/write action")
// Use it when when you need to make single action
let obj = AsyncObject<Int>(value: 0, dispatchQueueName: "Dispatch0")
obj.value = 100
let x = obj.value
print(x)

print("Write action in block")
// Use it when when you need to make many action
obj.setValue{ (current) -> (Int) in
    let newValue = current*2
    print("previous: \(current), new: \(newValue)")
    return newValue
}

Campione completo

estensione DispatchGroup

extension DispatchGroup {

    class func loop(repeatNumber: Int, action: @escaping (_ index: Int)->(), completion: @escaping ()->()) {
        let group = DispatchGroup()
        for index in 0...repeatNumber {
            group.enter()
            DispatchQueue.global(qos: .utility).async {
                action(index)
                group.leave()
            }
        }

        group.notify(queue: DispatchQueue.global(qos: .userInitiated)) {
            completion()
        }
    }
}

classe ViewController

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        //sample1()
        sample2()
    }

    func sample1() {
        print("=================================================\nsample with variable")

        let obj = AsyncObject<Int>(value: 0, dispatchQueueName: "Dispatch1")

        DispatchGroup.loop(repeatNumber: 5, action: { index in
            obj.value = index
        }) {
            print("\(obj.value)")
        }
    }

    func sample2() {
        print("\n=================================================\nsample with array")
        let arr = AsyncObject<[Int]>(value: [], dispatchQueueName: "Dispatch2")
        DispatchGroup.loop(repeatNumber: 15, action: { index in
            arr.setValue{ (current) -> ([Int]) in
                var array = current
                array.append(index*index)
                print("index: \(index), value \(array[array.count-1])")
                return array
            }
        }) {
            print("\(arr.value)")
        }
    }
}

1

Con i wrapper di proprietà di Swift, questo è quello che sto usando ora:

@propertyWrapper public struct NCCSerialized<Wrapped> {
    private let queue = DispatchQueue(label: "com.nuclearcyborg.NCCSerialized_\(UUID().uuidString)")

    private var _wrappedValue: Wrapped
    public var wrappedValue: Wrapped {
        get { queue.sync { _wrappedValue } }
        set { queue.sync { _wrappedValue = newValue } }
    }

    public init(wrappedValue: Wrapped) {
        self._wrappedValue = wrappedValue
    }
}

Quindi puoi semplicemente fare:

@NCCSerialized var foo: Int = 10

o

@NCCSerialized var myData: [SomeStruct] = []

Quindi accedi alla variabile come faresti normalmente.


1
Mi piace questa soluzione, ma ero curioso del costo della gente @Decorating poiché farlo ha l'effetto collaterale di creare un DispatchQueueoggetto nascosto all'utente. Ho trovato questo riferimento SO per mettermi a mio agio: stackoverflow.com/a/35022486/1060314
Adam Venturella,

L'involucro della proprietà in sé è abbastanza leggero - solo una struttura, quindi, una delle cose più leggere che puoi fare. Grazie per il link su DispatchQueue. Ho avuto nella parte posteriore della mia mente di fare alcuni test delle prestazioni sul wrapping queue.sync rispetto ad altre soluzioni (e rispetto a nessuna coda), ma non l'avevo fatto.
Drewster

1

In conclusione, Here fornisce un modo più comune che include valore di ritorno o vuoto e lancio

import Foundation

extension NSObject {


    func synchronized<T>(lockObj: AnyObject!, closure: () throws -> T) rethrows ->  T
    {
        objc_sync_enter(lockObj)
        defer {
            objc_sync_exit(lockObj)
        }

        return try closure()
    }


}

0

Perché renderlo difficile e seccante con le serrature? Usa le barriere di spedizione.

Una barriera di invio crea un punto di sincronizzazione all'interno di una coda simultanea.

Mentre è in esecuzione, nessun altro blocco sulla coda è autorizzato a funzionare, anche se è simultaneo e sono disponibili altri core.

Se suona come un blocco esclusivo (scrittura), lo è. I blocchi non barriera possono essere pensati come blocchi condivisi (di lettura).

Finché tutto l'accesso alla risorsa viene eseguito attraverso la coda, le barriere forniscono una sincronizzazione molto economica.


2
Voglio dire, stai presupponendo l'uso di una coda GCD per sincronizzare l'accesso, ma questo non è menzionato nella domanda originale. E una barriera è necessaria solo con una coda simultanea: puoi semplicemente usare una coda seriale per mettere in coda i blocchi esclusi reciprocamente per emulare un blocco.
Bill

La mia domanda, perché emulare un lucchetto? Da quello che ho letto, i blocchi sono scoraggiati a causa del sovraccarico contro una barriera all'interno di una coda.
Frederick C. Lee,

0

Basato su ɲeuroburɳ , prova un caso di sottoclasse

class Foo: NSObject {
    func test() {
        print("1")
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
            print("3")
        }

        print("2")
    }
}


class Foo2: Foo {
    override func test() {
        super.test()

        print("11")
        objc_sync_enter(self)
        defer {
            print("33")
            objc_sync_exit(self)
        }

        print("22")
    }
}

let test = Foo2()
test.test()

Produzione:

1
2
3
11
22
33

0

dispatch_barrier_async è il modo migliore, pur non bloccando il thread corrente.

dispatch_barrier_async (accessQueue, {dizionario [ID oggetto] = oggetto})


-5

Un altro metodo consiste nel creare una superclasse e quindi ereditarla. In questo modo puoi utilizzare GCD più direttamente

class Lockable {
    let lockableQ:dispatch_queue_t

    init() {
        lockableQ = dispatch_queue_create("com.blah.blah.\(self.dynamicType)", DISPATCH_QUEUE_SERIAL)
    }

    func lock(closure: () -> ()) {
        dispatch_sync(lockableQ, closure)
    }
}


class Foo: Lockable {

    func boo() {
        lock {
            ....... do something
        }
    }

10
-1 L'ereditarietà fornisce polimorfismo del sottotipo in cambio di un accoppiamento crescente. Evita il secondo se non hai bisogno del primo. Non essere pigro. Preferisce la composizione per il riutilizzo del codice.
Jano,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.