¿Cuál es el equivalente de Swift a "@synchronized" de Objective-C?


231

He buscado en el libro Swift, pero no puedo encontrar la versión Swift de @synchronized. ¿Cómo hago exclusión mutua en Swift?


1
Usaría una barrera de despacho. Las barreras proporcionan una sincronización muy barata. dispatch_barrier_async (). etc.
Frederick C. Lee

@ FrederickC.Lee, ¿qué pasa si necesita una escritura para sincronizar, como cuando se crea un contenedor removeFirst()?
ScottyBlades

Respuestas:


183

Puedes usar GCD. Es un poco más detallado que @synchronized, pero funciona como un reemplazo:

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

12
Esto es genial, pero carece de la capacidad de reingreso que tiene con @synchronized.
Michael Waterfall

99
Con este enfoque, debes tener cuidado. Su bloque podría ejecutarse en algún otro hilo. Los documentos de la API dicen: "Como una optimización, esta función invoca el bloque en el hilo actual cuando sea posible".
biografía

20
Gran artículo de Matt Gallagher sobre esto: cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html
wuf810

44
No, esto causa puntos muertos ocasionales.
Tom Kraina

71
No, no y no Buen intento, pero funciona imperfectamente bien. ¿Por qué? Lectura esencial (comparación exhaustiva de alternativas, precauciones) y un gran marco de utilidad de Matt Gallagher, aquí: cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html @ wuf810 mencionó esto primero (HT), pero subestimado lo bueno que es este artículo. Todos deberían leer. (Por favor, eleve esto al mínimo para que sea inicialmente visible, pero no más.)
primero

181

Estaba buscando esto yo mismo y llegué a la conclusión de que todavía no hay una construcción nativa dentro de Swift para esto.

Creé esta pequeña función auxiliar basada en algunos de los códigos que he visto de Matt Bridges y otros.

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

El uso es bastante sencillo

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

Hay un problema que he encontrado con esto. Pasar una matriz como el argumento de bloqueo parece causar un error de compilación muy obtuso en este punto. De lo contrario, parece funcionar como se desea.

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!

¡Agradable!
Presente

14
Esto es bastante útil y conserva la sintaxis del @synchronizedbloque muy bien, pero tenga en cuenta que no es idéntica a una declaración verdadero bloque incorporado como el @synchronizedbloque en Objective-C, porque returny breakdeclaraciones trabajo ya no saltar de la función / bucle que rodea al igual lo haría si fuera una declaración ordinaria.
newacct

3
El error es probablemente debido a que las matrices se pasan como valores no referencias
james_alvarez

99
Probablemente este sea un gran lugar para usar la nueva deferpalabra clave para garantizar que objc_sync_exitse llame incluso si se closurelanza.
devios1

3
@ t0rst Llamar a esta respuesta "defectuosa" según el artículo vinculado no es válida. El artículo dice que este método es "un poco más lento de lo ideal" y "está limitado a las plataformas de Apple". Eso no lo hace "defectuoso" por asomo.
RenniePet

151

Me gusta y utilizo muchas de las respuestas aquí, así que elegiría la que mejor funcione para usted. Dicho esto, el método que prefiero cuando necesito algo como el objetivo-c @synchronizedutiliza la deferdeclaración introducida en swift 2.

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

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

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

Lo bueno de este método, es que su sección crítica puede salir del bloque de contención de cualquier manera deseada (por ejemplo, return, break, continue, throw), y "las declaraciones dentro de la declaración de aplazamiento se ejecutan sin importar cómo se transfiere el control del programa." 1


Creo que esta es probablemente la solución más elegante que se proporciona aquí. Gracias por sus comentarios.
Scott D

3
¿Qué es lock? ¿Cómo se lockinicializa?
Van Du Tran

66
lockes cualquier objeto objetivo-c.
uroeuroburɳ

1
¡Excelente! Había escrito algunos métodos de bloqueo de ayuda cuando se introdujo Swift 1 y no los había revisado en mucho tiempo. Se olvidó por completo del aplazamiento; ¡Este es el camino a seguir!
Randy

Me gusta, pero aparece un error del compilador "El bloque de sentencias entre paréntesis es un cierre no utilizado" en Xcode 8. Ah, lo entiendo, son solo las llaves de función, demasiado tiempo para encontrar su enlace de referencia "1", ¡gracias!
Duncan Groenewald

83

Puede intercalar declaraciones entre objc_sync_enter(obj: AnyObject?)y objc_sync_exit(obj: AnyObject?). La palabra clave @synchronized está utilizando esos métodos debajo de las cubiertas. es decir

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

3
¿Se considerará esto el uso de una API privada por parte de Apple?
Drux

2
No, objc_sync_entery objc_sync_exitson métodos definidos en Objc-sync.h y son de código abierto: opensource.apple.com/source/objc4/objc4-371.2/runtime/…
bontoJR

¿Qué sucede si varios subprocesos intentan acceder al mismo recurso, el segundo espera, vuelve a intentarlo o se bloquea?
TruMan1

Agregando a lo que dijo @bontoJR, objc_sync_enter(…)y objc_sync_exit(…)son encabezados públicos proporcionados por iOS / macOS / etc. API (parece que están dentro ….sdkde la ruta usr/include/objc/objc-sync.h) . La forma más fácil de averiguar si algo es una API pública o no es (en Xcode) escribir el nombre de la función (por ejemplo objc_sync_enter(), no es necesario especificar argumentos para las funciones C) , luego intente hacer clic con el comando. Si le muestra el archivo de encabezado para esa API, entonces está bien (ya que no podría ver el encabezado si no fuera público) .
Slipp D. Thompson

75

El análogo de la @synchronizeddirectiva de Objective-C puede tener un tipo de retorno arbitrario y un buen rethrowscomportamiento en 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()
}

El uso de la deferdeclaración permite devolver directamente un valor sin introducir una variable temporal.


En Swift 2 agregue el @noescapeatributo al cierre para permitir más optimizaciones:

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

Basado en las respuestas de GNewc [1] (donde me gusta el tipo de retorno arbitrario) y Tod Cunningham [2] (donde me gusta defer).


Xcode me dice que @noescape ahora es el predeterminado y está en desuso en Swift 3.
RenniePet

Así es, el código en esta respuesta es para Swift 2 y requiere alguna adaptación para Swift 3. Lo actualizaré cuando tenga tiempo.
werediver

1
¿Puedes explicar el uso? Tal vez con un ejemplo ... gracias de antemano! En mi caso, tengo un conjunto que necesito sincronizar, porque manipulo su contenido en un DispatchQueue.
sancho

@sancho Prefiero mantener esta publicación concisa. Parece preguntar sobre pautas generales de programación concurrente, esa es una pregunta amplia. ¡Intenta hacerla como una pregunta separada!
werediver 01 de

41

SWIFT 4

En Swift 4 puede usar las colas de despacho de GCD para bloquear recursos.

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

Esto no parece funcionar con XCode8.1. .serialParece no estar disponible. Pero .concurrentestá disponible. : /
Travis Griggs

2
el valor predeterminado es .serial
Duncan Groenewald

2
Tenga en cuenta que este patrón no protege adecuadamente contra los problemas más comunes de subprocesos múltiples. Por ejemplo, si ejecutara myObject.state = myObject.state + 1simultáneamente, no contaría las operaciones totales, sino que arrojaría un valor no determinista. Para resolver ese problema, el código de llamada debe estar envuelto en una cola en serie para que tanto la lectura como la escritura ocurran atómicamente. Por supuesto, Obj-c @synchronisedtiene el mismo problema, por lo que su implementación es correcta.
Berik

1
Sí, myObject.state += 1es una combinación de una operación de lectura y luego de escritura. Algún otro hilo aún puede interponerse para establecer / escribir un valor. Según objc.io/blog/2018/12/18/atomic-variables , sería más fácil ejecutarlo seten un bloque / cierre de sincronización y no bajo la variable misma.
CyberMew

24

Utilizando la respuesta de Bryan McLemore, la extendí para apoyar objetos que arrojan una mansión segura con la habilidad de aplazamiento Swift 2.0.

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

    try block()
}

Sería mejor usarlo rethrowspara simplificar el uso con cierres sin tirar (no es necesario usar try), como se muestra en mi respuesta .
werediver

23

Para agregar la funcionalidad de retorno, puede hacer esto:

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

Posteriormente, puede llamarlo 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

Este código tiene la capacidad de reingreso y puede funcionar con llamadas de función asincrónicas. En este código, después de llamar a someAsyncFunc (), se procesará otro cierre de función en la cola en serie, pero semaphore.wait () bloqueará hasta que se llame a signal (). internalQueue.sync no debe usarse, ya que bloqueará el hilo principal si no me equivoco.

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 no es una buena idea sin manejo de errores.


¿Qué error de manejo? El compilador no permitirá nada que arroje. Por otro lado, al no usar objc_sync_enter / exit, renuncia a algunas ganancias sustanciales de rendimiento.
gnasher729

8

En la sesión 414 "Comprender los bloqueos y los registros de bloqueos" de la WWDC 2018, muestran la siguiente manera utilizando DispatchQueues con sincronización.

En swift 4 debería ser algo como lo siguiente:

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

De todos modos, también puede hacer lecturas más rápidas utilizando colas concurrentes con barreras. Las lecturas de sincronización y asíncrona se realizan simultáneamente y la escritura de un nuevo valor espera a que finalicen las operaciones anteriores.

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

probablemente no necesite bloquear las lecturas y ralentizar la cola usando la sincronización. Puede usar la sincronización para la escritura en serie.
Basheer_CAD

6

Use NSLock en 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()

Advertencia La clase NSLock usa hilos POSIX para implementar su comportamiento de bloqueo. Al enviar un mensaje de desbloqueo a un objeto NSLock, debe asegurarse de que el mensaje se envíe desde el mismo hilo que envió el mensaje de bloqueo inicial. Desbloquear un bloqueo de un hilo diferente puede provocar un comportamiento indefinido.



6

En el moderno Swift 5, con capacidad de retorno:

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

Úselo así, para aprovechar la capacidad de valor de retorno:

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

O así de otra manera:

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

2
Esta es la respuesta correcta y no la aceptada y altamente votada (que depende de GCD). Parece que esencialmente nadie usa o entiende cómo usar Thread. Estoy muy contento con eso, mientras que GCDestá lleno de problemas y limitaciones.
Java

4

Prueba: NSRecursiveLock

Un bloqueo que puede ser adquirido varias veces por el mismo hilo sin causar un punto muerto.

let lock = NSRecursiveLock()

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

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

2

Figura Publicaré mi implementación de Swift 5, basada en las respuestas anteriores. ¡Gracias chicos! Me pareció útil tener uno que también devuelva un valor, así que tengo dos métodos.

Aquí hay una clase simple para hacer primero:

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

Luego úselo así si necesita un valor de retorno:

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

O:

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

Intenta public class func synced<T>(_ lock: Any, closure: () -> T), funciona para ambos, nulo y cualquier otro tipo. También está el material de rebrote.
hnh

@hnh, ¿qué quieres decir con cosas de rebrote? Además, si desea compartir una llamada de ejemplo al método genérico con el tipo <T> que me ayudaría a actualizar la respuesta, me gusta a dónde va con eso.
TheJeff

vuelve a lanzar, no vuelve a crecer, srz
hnh

1

Detalles

xCode 8.3.1, swift 3.1

Tarea

Leer el valor de escritura de diferentes hilos (asíncrono).

Código

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
}

Muestra completa

extensión 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()
        }
    }
}

clase 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 los envoltorios de propiedades de Swift, esto es lo que estoy usando ahora:

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

Entonces puedes simplemente hacer:

@NCCSerialized var foo: Int = 10

o

@NCCSerialized var myData: [SomeStruct] = []

Luego acceda a la variable como lo haría normalmente.


1
Me gusta esta solución, pero tenía curiosidad sobre el costo de la gente @Decorating ya que hacerlo tiene el efecto secundario de crear uno DispatchQueueque está oculto para el usuario. Encontré esta referencia SO para tranquilizarme: stackoverflow.com/a/35022486/1060314
Adam Venturella

El contenedor de propiedades en sí es bastante ligero, solo una estructura, por lo que es una de las cosas más ligeras que puede hacer. Gracias por el enlace en DispatchQueue sin embargo. En el fondo de mi mente he tenido que hacer algunas pruebas de rendimiento en la cola.sync wrap frente a otras soluciones (y frente a ninguna cola), pero no lo había hecho.
Drewster

1

En conclusión, aquí damos una forma más común que incluye el valor de retorno o nulo, y arrojamos

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

¿Por qué hacerlo difícil y molesto con las cerraduras? Utilice barreras de envío.

Una barrera de despacho crea un punto de sincronización dentro de una cola concurrente.

Mientras se está ejecutando, no se permite ejecutar ningún otro bloque en la cola, incluso si es concurrente y hay otros núcleos disponibles.

Si eso suena como un bloqueo exclusivo (escritura), lo es. Los bloques sin barrera pueden considerarse bloqueos compartidos (leídos).

Siempre que todo el acceso al recurso se realice a través de la cola, las barreras proporcionan una sincronización muy barata.


2
Quiero decir, estás asumiendo el uso de una cola GCD para sincronizar el acceso, pero eso no se menciona en la pregunta original. Y una barrera solo es necesaria con una cola concurrente: simplemente puede usar una cola en serie para poner en cola bloques mutuamente excluidos para emular un bloqueo.
Bill

Mi pregunta, ¿por qué emular una cerradura? Por lo que leí, los bloqueos se desaconsejan debido a la sobrecarga frente a una barrera dentro de una cola.
Frederick C. Lee

0

Basado en ɳeuroburɳ , pruebe un caso de subclase

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

Salida:

1
2
3
11
22
33

0

dispatch_barrier_async es la mejor manera, sin bloquear el hilo actual.

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


-5

Otro método es crear una superclase y luego heredarla. De esta manera puedes usar GCD más directamente

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 La herencia te da un polimorfismo de subtipo a cambio de aumentar el acoplamiento. Evita lo posterior si no necesitas lo primero. No seas perezoso Prefiere la composición para la reutilización del código.
Jano
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.