Quel est l'équivalent Swift de «@synchronized» d'Objective-C?

231

J'ai cherché dans le livre Swift, mais je ne trouve pas la version Swift de @synchronized. Comment puis-je faire de l'exclusion mutuelle dans Swift?

Facture
la source
1
J'utiliserais une barrière d'expédition. Les barrières offrent une synchronisation très bon marché. dispatch_barrier_async (). etc.
Frederick C. Lee
@ FrederickC.Lee, que se passe-t-il si vous avez besoin d'une synchronisation d' écriture , comme lors de la création d'un wrapper pour removeFirst()?
ScottyBlades

Réponses:

183

Vous pouvez utiliser GCD. Il est un peu plus verbeux que @synchronized, mais fonctionne comme un remplacement:

let serialQueue = DispatchQueue(label: "com.test.mySerialQueue")
serialQueue.sync {
    // code
}
conmulligan
la source
12
C'est très bien, mais il manque la capacité de rentrée que vous avez avec @synchronized.
Michael Waterfall
9
Avec cette approche, vous devez être prudent. Votre bloc peut être exécuté sur un autre thread. Les documents API disent: "En tant qu'optimisation, cette fonction appelle le bloc sur le thread actuel lorsque cela est possible."
bio
20
Excellent article de Matt Gallagher à ce sujet: cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html
wuf810
4
Non, cela provoque des blocages occasionnels.
Tom Kraina
71
Non, non et non. Bien essayé, mais fonctionne imparfaitement bien. Pourquoi? Une lecture essentielle (comparaison complète des alternatives, mises en garde) et un excellent cadre d'utilité de Matt Gallagher, ici: cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html @ wuf810 ont mentionné ce premier (HT), mais sous-estimé la qualité de ces articles. Tous devraient lire. (S'il vous plaît, votez ce nombre au minimum pour le rendre initialement visible, mais pas plus.)
premier
181

Je le cherchais moi-même et je suis arrivé à la conclusion qu'il n'y avait pas encore de construction native dans swift pour cela.

J'ai créé cette petite fonction d'aide basée sur une partie du code que j'ai vu de Matt Bridges et d'autres.

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

L'utilisation est assez simple

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

Il y a un problème que j'ai trouvé avec cela. Passer dans un tableau en tant qu'argument de verrouillage semble provoquer une erreur de compilation très obtuse à ce stade. Sinon, cela semble fonctionner comme souhaité.

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!
Bryan McLemore
la source
Agréable! Veuillez déposer un bogue pour cela si c'est toujours un problème dans 1.0
MattD
14
Ceci est assez utile et préserve bien la syntaxe du @synchronizedbloc, mais notez qu'il n'est pas identique à une véritable instruction de bloc intégrée comme le @synchronizedbloc dans Objective-C, car les instructions returnet breakne fonctionnent plus pour sortir de la fonction / boucle environnante comme ce serait le cas s'il s'agissait d'une déclaration ordinaire.
newacct
3
Une erreur est probablement due au fait que les tableaux sont passés en tant que valeurs et non références
james_alvarez
9
Ce serait probablement un excellent endroit pour utiliser le nouveau defermot clé pour vous assurer d' objc_sync_exitêtre appelé même en cas de closurelancement.
devios1
3
@ t0rst L'appel de cette réponse "imparfait" basé sur l'article lié n'est pas valide. L'article indique que cette méthode est "un peu plus lente que l'idéal" et "est limitée aux plates-formes Apple". Cela ne le rend pas "défectueux" par un long shot.
RenniePet
151

J'aime et utilise de nombreuses réponses ici, donc je choisirais celle qui vous convient le mieux. Cela dit, la méthode que je préfère lorsque j'ai besoin de quelque chose comme objective-c @synchronizedutilise la deferdéclaration introduite dans 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 chose gentille au sujet de cette méthode, est que votre section critique peut sortir du bloc contenant une manière désirée quelconque (par exemple, return, break, continue, throw), et « les déclarations dans l'instruction defer sont exécutées , peu importe la façon dont le contrôle du programme est transféré. » 1

ɲeuroburɳ
la source
Je pense que c'est probablement la solution la plus élégante fournie ici. Merci pour vos commentaires.
Scott D
3
Qu'est-ce que c'est lock? Comment est lockinitialisé?
Van Du Tran
6
lockest tout objet objectif-c.
ɲeuroburɳ
1
Excellent! J'avais écrit des méthodes d'aide au verrouillage lorsque Swift 1 a été introduit et je ne les avais pas revues depuis longtemps. J'ai complètement oublié le report; c'est la voie à suivre!
Randy
J'aime cela, mais j'obtiens une erreur de compilation «Le bloc d'instructions contreventé est une fermeture inutilisée» dans Xcode 8. Ah, je comprends que ce ne sont que les accolades de fonction - trop de temps pour trouver votre lien de référence «1» - merci!
Duncan Groenewald
83

Vous pouvez prendre en sandwich les déclarations entre objc_sync_enter(obj: AnyObject?)et objc_sync_exit(obj: AnyObject?). Le mot clé @synchronized utilise ces méthodes sous les couvertures. c'est à dire

objc_sync_enter(self)
... synchronized code ...
objc_sync_exit(self)
Matt Bridges
la source
3
Est-ce que cela sera considéré comme l'utilisation d'une API privée par Apple?
Drux
2
Non, objc_sync_enteret objc_sync_exitles méthodes sont définies dans Objc-sync.h et sont open source: opensource.apple.com/source/objc4/objc4-371.2/runtime/…
bontoJR
Que se passe-t-il si plusieurs threads tentent d'accéder à la même ressource, le second attend-il, réessaye-t-il ou plante-il?
TruMan1
Ajoutant à ce que @bontoJR a dit, objc_sync_enter(…)et objc_sync_exit(…)sont des en-têtes publics fournis par iOS / macOS / etc. API (on dirait qu'elles sont à l'intérieur ….sdkdu chemin d'accès usr/include/objc/objc-sync.h) . La façon la plus simple de savoir si quelque chose est une API publique ou non est de (dans Xcode) taper le nom de la fonction (par exemple objc_sync_enter(); les arguments n'ont pas besoin d'être spécifiés pour les fonctions C) , puis essayez de cliquer avec la commande. S'il vous montre le fichier d'en-tête pour cette API, alors vous êtes bon (puisque vous ne pourriez pas voir l'en-tête s'il n'était pas public) .
Slipp D. Thompson,
75

L'analogue de la @synchronizeddirective d'Objective-C peut avoir un type de retour arbitraire et un rethrowscomportement agréable dans 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'utilisation de l' deferinstruction permet de renvoyer directement une valeur sans introduire de variable temporaire.


Dans Swift 2, ajoutez l' @noescapeattribut à la fermeture pour permettre plus d'optimisations:

// 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()
}

Basé sur les réponses de GNewc [1] (où j'aime le type de retour arbitraire) et Tod Cunningham [2] (où j'aime defer).

plongeur
la source
Xcode me dit que @noescape est maintenant par défaut et est déconseillé dans Swift 3.
RenniePet
C'est vrai, le code dans cette réponse est pour Swift 2 et nécessite une adaptation pour Swift 3. Je le mettrai à jour quand j'en aurai le temps.
werediver
1
Pouvez-vous expliquer l'utilisation? Peut-être avec un exemple .. merci d'avance! Dans mon cas, j'ai un ensemble que je dois synchroniser, car je manipule son contenu dans une DispatchQueue.
sancho
@sancho Je préfère garder ce post concis. Vous semblez poser des questions sur les directives générales de programmation concurrente, c'est une vaste question. Essayez de le poser comme une question distincte!
werediver
41

SWIFT 4

Dans Swift 4, vous pouvez utiliser les files d'attente de répartition GCD pour verrouiller les ressources.

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 }
        }
    }
} 
Sebastian Boldt
la source
Cela ne semble pas fonctionner avec XCode8.1. .serialsemble indisponible. Mais .concurrentest disponible. : /
Travis Griggs
2
la valeur par défaut est .serial
Duncan Groenewald
2
Notez que ce modèle ne protège pas correctement contre les problèmes multi-threads les plus courants. Par exemple, si vous exécutez myObject.state = myObject.state + 1simultanément, il ne comptera pas le nombre total d'opérations mais produira à la place une valeur non déterministe. Pour résoudre ce problème, le code appelant doit être encapsulé dans une file d'attente série afin que la lecture et l'écriture se produisent de manière atomique. Bien sûr, Obj-c @synchroniseda le même problème, donc dans ce sens, votre implémentation est correcte.
Berik
1
Oui, myObject.state += 1est une combinaison d'une opération de lecture puis d'écriture. Un autre thread peut encore entrer entre définir et écrire une valeur. Selon objc.io/blog/2018/12/18/atomic-variables , il serait plus facile d'exécuter le setdans un bloc de synchronisation / fermeture à la place et non sous la variable elle-même.
CyberMew
24

En utilisant la réponse de Bryan McLemore, je l'ai étendu pour prendre en charge les objets qui jettent dans un manoir sûr avec la capacité de report de Swift 2.0.

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

    try block()
}
Tod Cunningham
la source
Il serait préférable d'utiliser rethrowspour simplifier l'utilisation avec des fermetures sans lancement (pas besoin d'utiliser try), comme indiqué dans ma réponse .
werediver
23

Pour ajouter une fonction de retour, vous pouvez procéder comme suit:

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

Par la suite, vous pouvez l'appeler en utilisant:

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

Swift 3

Ce code a la possibilité de rentrer et peut fonctionner avec les appels de fonction asynchrones. Dans ce code, après l'appel de someAsyncFunc (), une autre fermeture de fonction sur la file d'attente série sera traitée mais bloquée par semaphore.wait () jusqu'à ce que signal () soit appelé. internalQueue.sync ne doit pas être utilisé car il bloquera le thread principal si je ne me trompe pas.

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 n'est pas une bonne idée sans gestion des erreurs.

Hanny
la source
Quelle gestion des erreurs? Le compilateur n'autorisera rien qui lève. En revanche, en n'utilisant pas objc_sync_enter / exit, vous abandonnez certains gains de performances substantiels.
gnasher729
8

Dans la session "Understanding Crashes and Crash Logs" 414 de la WWDC 2018, ils montrent la manière suivante d'utiliser DispatchQueues avec synchronisation.

Dans swift 4 devrait être quelque chose comme ceci:

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
          }
        }
    }
}

Quoi qu'il en soit, vous pouvez également effectuer des lectures plus rapidement en utilisant des files d'attente simultanées avec des barrières. Les lectures synchronisées et asynchrones sont effectuées simultanément et l'écriture d'une nouvelle valeur attend la fin des opérations précédentes.

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
        }
    }
}
rockdaswift
la source
vous n'avez probablement pas besoin de bloquer les lectures et de ralentir la file d'attente à l'aide de la synchronisation. Vous pouvez simplement utiliser la synchronisation pour l'écriture en série.
Basheer_CAD
6

Utilisez NSLock dans 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()

Avertissement La classe NSLock utilise des threads POSIX pour implémenter son comportement de verrouillage. Lors de l'envoi d'un message de déverrouillage à un objet NSLock, vous devez être sûr que le message est envoyé à partir du même thread qui a envoyé le message de verrouillage initial. Déverrouiller un verrou d'un thread différent peut entraîner un comportement indéfini.

DàChún
la source
6

Dans le Swift 5 moderne, avec possibilité de retour:

/**
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()
}

Utilisez-le comme ceci, pour profiter de la capacité de valeur de retour:

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

Ou comme ça autrement:

synchronized(self) { 
     // Your code here
    yourCode()
}
Stéphane de Luca
la source
2
C'est la bonne réponse et non celle acceptée et très appréciée (qui dépend GCD). Il semble que personne n'utilise ou ne comprenne comment l'utiliser Thread. J'en suis très satisfait - alors qu'il GCDest semé d'embûches et de limitations.
javadba
4

Essayez: NSRecursiveLock

Un verrou qui peut être acquis plusieurs fois par le même thread sans provoquer de blocage.

let lock = NSRecursiveLock()

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

func f2() {
    lock.lock()
    defer {
        lock.unlock()
    }
    //Your Code
}
ZevsVU
la source
2

Je vais publier mon implémentation de Swift 5, basée sur les réponses précédentes. Merci les gars! J'ai trouvé utile d'en avoir un qui renvoie également une valeur, j'ai donc deux méthodes.

Voici une classe simple à faire en premier:

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()
    }
}

Ensuite, utilisez-le comme si vous aviez besoin d'une valeur de retour:

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

Ou:

Sync.synced(self, closure: {
    // do some work synchronously
})
TheJeff
la source
Essayez public class func synced<T>(_ lock: Any, closure: () -> T), fonctionne pour les deux, nul et tout autre type. Il y a aussi les trucs de repousse.
hnh
@hnh qu'entendez-vous par les trucs de repousse? De plus, si vous souhaitez partager un exemple d'appel à la méthode générique avec le type <T> qui m'aiderait à mettre à jour la réponse - j'aime où vous allez avec cela.
TheJeff
rethrows, non regrows, srz
hnh
1

Détails

xCode 8.3.1, swift 3.1

Tâche

Lire la valeur d'écriture à partir de différents threads (async).

Code

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)"
    }
}

Usage

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
}

Échantillon complet

extension 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)")
        }
    }
}
Vasily Bodnarchuk
la source
1

Avec les wrappers de propriétés de Swift, voici ce que j'utilise maintenant:

@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
    }
}

Ensuite, vous pouvez simplement faire:

@NCCSerialized var foo: Int = 10

ou

@NCCSerialized var myData: [SomeStruct] = []

Accédez ensuite à la variable comme vous le feriez normalement.

drewster
la source
1
J'aime cette solution, mais j'étais curieux de connaître le coût des gens @ décorer, car cela a pour effet secondaire de créer un DispatchQueuequi est caché à l'utilisateur. J'ai trouvé cette référence SO pour me mettre à l'aise: stackoverflow.com/a/35022486/1060314
Adam Venturella
L'encapsuleur de propriété lui-même est assez léger - juste une structure, donc, l'une des choses les plus légères que vous puissiez faire. Merci pour le lien sur DispatchQueue. J'ai eu à l'esprit de faire des tests de performances sur le wrapper queue.sync par rapport à d'autres solutions (et par rapport à aucune file d'attente), mais je ne l'avais pas fait.
Drewster
1

En conclusion, voici un moyen plus courant qui inclut la valeur de retour ou void, et lancez

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()
    }


}
Victor Choy
la source
0

Pourquoi le rendre difficile et compliqué avec des serrures? Utilisez des barrières d'expédition.

Une barrière de répartition crée un point de synchronisation dans une file d'attente simultanée.

Pendant son exécution, aucun autre bloc de la file d'attente n'est autorisé à s'exécuter, même s'il est simultané et que d'autres cœurs sont disponibles.

Si cela ressemble à un verrou exclusif (écriture), ça l'est. Les blocs sans barrière peuvent être considérés comme des verrous partagés (en lecture).

Tant que tous les accès à la ressource sont effectués via la file d'attente, les barrières offrent une synchronisation très bon marché.

Frederick C. Lee
la source
2
Je veux dire, vous supposez l'utilisation d'une file d'attente GCD pour synchroniser l'accès, mais cela n'est pas mentionné dans la question d'origine. Et une barrière n'est nécessaire qu'avec une file d'attente simultanée - vous pouvez simplement utiliser une file d'attente série pour mettre en file d'attente des blocs mutuellement exclus pour émuler un verrou.
Bill
Ma question, pourquoi émuler un verrou? D'après ce que j'ai lu, les verrous sont découragés en raison de la surcharge par rapport à une barrière dans une file d'attente.
Frederick C. Lee
0

Basé sur ɲeuroburɳ , testez un cas de sous-classe

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()

Production:

1
2
3
11
22
33
AechoLiu
la source
0

dispatch_barrier_async est le meilleur moyen, sans bloquer le thread actuel.

dispatch_barrier_async (accessQueue, {dictionary [object.ID] = object})

Jacky
la source
-5

Une autre méthode consiste à créer une superclasse puis à en hériter. De cette façon, vous pouvez utiliser GCD plus directement

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
        }
    }
Jim
la source
10
-1 L'héritage vous donne un polymorphisme de sous-type en échange d'un couplage croissant. Évitez le dernier si vous n'avez pas besoin du premier. Ne sois pas paresseux. Préférez la composition pour la réutilisation du code.
Jano