dispatch_once después de que cambie la API de Swift 3 GCD


84

¿Para qué sirve la nueva sintaxis dispatch_onceen Swift después de los cambios realizados en la versión de idioma 3? La versión anterior era la siguiente.

var token: dispatch_once_t = 0
func test() {
    dispatch_once(&token) {
    }
}

Estos son los cambios realizados en libdispatch .



Respuestas:


70

Del doc :

Despacho
La función gratuita dispatch_once ya no está disponible en Swift. En Swift, puede usar propiedades globales o estáticas inicializadas de manera perezosa y obtener las mismas garantías de seguridad de subprocesos y llamadas una vez que dispatch_once. Ejemplo:

let myGlobal: () = { … global contains initialization in a call to a closure … }()
_ = myGlobal  // using myGlobal will invoke the initialization code only the first time it is used.

3
No es como si no supieras que Swift cambiaría rápidamente y tendrías que corregir una gran cantidad de código roto entre las versiones de Swift.
Abizern

2
el mayor problema son las cápsulas de terceros que no siempre son compatibles con Swift3.
Tinkerbell

4
Esa es la deuda técnica que acumula al introducir dependencias de terceros, @Tinkerbell. Me encanta Swift, pero soy muy cauteloso al incorporar dependencias externas que lo usan por esta misma razón.
Chris Wagner

17
dispatch_onceestaba claro. Esto, desafortunadamente, es feo y confuso ..
Alexandre G

104

Si bien el uso de globales inicializados diferidos puede tener sentido para una inicialización única, no tiene sentido para otros tipos. Tiene mucho sentido usar globales perezosos inicializados para cosas como singletons, no tiene mucho sentido para cosas como proteger una configuración de swizzle.

Aquí hay una implementación estilo Swift 3 de dispatch_once:

public extension DispatchQueue {

    private static var _onceTracker = [String]()

    /**
     Executes a block of code, associated with a unique token, only once.  The code is thread safe and will
     only execute the code once even in the presence of multithreaded calls.

     - parameter token: A unique reverse DNS style name such as com.vectorform.<name> or a GUID
     - parameter block: Block to execute once
     */
    public class func once(token: String, block:@noescape(Void)->Void) {
        objc_sync_enter(self); defer { objc_sync_exit(self) }

        if _onceTracker.contains(token) {
            return
        }

        _onceTracker.append(token)
        block()
    }
}

Aquí hay un ejemplo de uso:

DispatchQueue.once(token: "com.vectorform.test") {
    print( "Do This Once!" )
}

o usando un UUID

private let _onceToken = NSUUID().uuidString

DispatchQueue.once(token: _onceToken) {
    print( "Do This Once!" )
}

Dado que actualmente estamos en un momento de transición de Swift 2 a 3, aquí hay un ejemplo de implementación de Swift 2:

public class Dispatch
{
    private static var _onceTokenTracker = [String]()

    /**
     Executes a block of code, associated with a unique token, only once.  The code is thread safe and will
     only execute the code once even in the presence of multithreaded calls.

     - parameter token: A unique reverse DNS style name such as com.vectorform.<name> or a GUID
     - parameter block: Block to execute once
     */
    public class func once(token token: String, @noescape block:dispatch_block_t) {
        objc_sync_enter(self); defer { objc_sync_exit(self) }

        if _onceTokenTracker.contains(token) {
            return
        }

        _onceTokenTracker.append(token)
        block()
    }

}

Muchas gracias por la solucion Estaba exactamente atrapado en una configuración de swizzle. Espero que el equipo de Swift aborde este caso de uso.
salman140

2
Absolutamente no deberías usar objc_sync_entery objc_sync_exitmás.
smat88dd

1
¿Y por qué es eso?
Tod Cunningham

1
Para el rendimiento, debe usar un conjunto en lugar de una matriz para _onceTrackers. Esto mejora la complejidad del tiempo de O (N) a O (1).
Werner Altewischer

2
Así que escribe una clase reutilizable asumiendo que no se reutilizará tanto :-) Si no requiere un esfuerzo adicional para reducir la complejidad del tiempo de O (N) a O (1), siempre debe hacerlo en mi humilde opinión.
Werner Altewischer

62

Ampliando la respuesta de Tod Cunningham anterior, agregué otro método que crea el token automáticamente desde el archivo, la función y la línea.

public extension DispatchQueue {
    private static var _onceTracker = [String]()

    public class func once(file: String = #file,
                           function: String = #function,
                           line: Int = #line,
                           block: () -> Void) {
        let token = "\(file):\(function):\(line)"
        once(token: token, block: block)
    }

    /**
     Executes a block of code, associated with a unique token, only once.  The code is thread safe and will
     only execute the code once even in the presence of multithreaded calls.

     - parameter token: A unique reverse DNS style name such as com.vectorform.<name> or a GUID
     - parameter block: Block to execute once
     */
    public class func once(token: String,
                           block: () -> Void) {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }

        guard !_onceTracker.contains(token) else { return }

        _onceTracker.append(token)
        block()
    }
}

Entonces puede ser más simple llamar:

DispatchQueue.once {
    setupUI()
}

y aún puede especificar un token si lo desea:

DispatchQueue.once(token: "com.hostname.project") {
    setupUI()
}

Supongo que podría producirse una colisión si tiene el mismo archivo en dos módulos. Lástima que no haya#module


esto arroja más luz sobre lo que está pasando. Gracias.
nyxee


Realmente ayudó a Thanx
Manjunath C.Kadani

19

Editar

La respuesta de @ Frizlab: no se garantiza que esta solución sea segura para subprocesos. Se debe utilizar una alternativa si esto es crucial

La solución simple es

lazy var dispatchOnce : Void  = { // or anyName I choose

    self.title = "Hello Lazy Guy"

    return
}()

usado como

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    _ = dispatchOnce
}

1
Esto no ayuda en absoluto, porque una declaración de var lazy no se puede hacer en línea con el código regular, tiene que estar en una estructura o definición de clase. Eso significa que el contenido de dispatchOnce no puede capturar el alcance circundante de una instancia. Por ejemplo, si declara un cierre que aún no se ha ejecutado, no puede declarar la estructura dentro de ese cierre y hacer que el contenido de la var perezosa sea otro cierre que capture vars del cierre circundante ...
CommaToast

3
Se votó en contra porque este código definitivamente no tiene la misma semántica que dispatch_once. dispatch_once garantiza que el código se ejecute exactamente una vez, independientemente del hilo desde el que lo llame . Las vars perezosas tienen un comportamiento indefinido en un entorno de subprocesos múltiples.
Frizlab

en esta solución, el bloque init va a llamar dos veces en algunos casos
sagrado

8

Aún puede usarlo si agrega un encabezado de puente:

typedef dispatch_once_t mxcl_dispatch_once_t;
void mxcl_dispatch_once(mxcl_dispatch_once_t *predicate, dispatch_block_t block);

Luego en .malgún lugar:

void mxcl_dispatch_once(mxcl_dispatch_once_t *predicate, dispatch_block_t block) {
    dispatch_once(predicate, block);
}

Ahora debería poder utilizar mxcl_dispatch_onceSwift.

En su mayoría, debería usar lo que Apple sugiere, pero tuve algunos usos legítimos en los que necesitaba dispatch_oncecon un solo token en dos funciones y no está cubierto por lo que Apple proporciona.


7

Puede declarar una función variable de nivel superior como esta:

private var doOnce: ()->() = {
    /* do some work only once per instance */
    return {}
}()

luego llame a esto en cualquier lugar:

doOnce()

1
Las vars perezosas tienen el alcance de la clase, por lo que esto no actuará como dispatch_once. Se ejecutará una vez por instancia de la clase subyacente. Muévalo fuera de la clase [private var doOnce: () -> () = {}] o márquelo estático [static var doOnce: () -> () = {}]
Eli Burke

1
¡Absolutamente correcto! Gracias. En la mayoría de los casos, necesitará una acción por instancia.
Bogdan Novikov

2
¡Esta es una gran solución! Elegante, corto y claro
Ben Leggiero

6

Swift 3: para aquellos a los que les gustan las clases (o estructuras) reutilizables:

public final class /* struct */ DispatchOnce {
   private var lock: OSSpinLock = OS_SPINLOCK_INIT
   private var isInitialized = false
   public /* mutating */ func perform(block: (Void) -> Void) {
      OSSpinLockLock(&lock)
      if !isInitialized {
         block()
         isInitialized = true
      }
      OSSpinLockUnlock(&lock)
   }
}

Uso:

class MyViewController: UIViewController {

   private let /* var */ setUpOnce = DispatchOnce()

   override func viewWillAppear() {
      super.viewWillAppear()
      setUpOnce.perform {
         // Do some work here
         // ...
      }
   }

}

Actualización (28 de abril de 2017): OSSpinLockreemplazada con os_unfair_lockadvertencias de obsolescencia debida en macOS SDK 10.12.

public final class /* struct */ DispatchOnce {
   private var lock = os_unfair_lock()
   private var isInitialized = false
   public /* mutating */ func perform(block: (Void) -> Void) {
      os_unfair_lock_lock(&lock)
      if !isInitialized {
         block()
         isInitialized = true
      }
      os_unfair_lock_unlock(&lock)
   }
}

Recibo un mensaje de que OSSSpinLock está obsoleto en iOS 10.0
markhorrocks

2
¡Gracias! Código de ejemplo actualizado. OSSpinLockreemplazado con os_unfair_lock. Por cierto: Aquí hay un buen video de la WWDC sobre Concurrent Programming: developer.apple.com/videos/play/wwdc2016/720
Vlad

0

Mejoro las respuestas anteriores obtengo resultado:

import Foundation
extension DispatchQueue {
    private static var _onceTracker = [AnyHashable]()

    ///only excute once in same file&&func&&line
    public class func onceInLocation(file: String = #file,
                           function: String = #function,
                           line: Int = #line,
                           block: () -> Void) {
        let token = "\(file):\(function):\(line)"
        once(token: token, block: block)
    }

    ///only excute once in same Variable
    public class func onceInVariable(variable:NSObject, block: () -> Void){
        once(token: variable.rawPointer, block: block)
    }
    /**
     Executes a block of code, associated with a unique token, only once.  The code is thread safe and will
     only execute the code once even in the presence of multithreaded calls.

     - parameter token: A unique reverse DNS style name such as com.vectorform.<name> or a GUID
     - parameter block: Block to execute once
     */
    public class func once(token: AnyHashable,block: () -> Void) {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }

        guard !_onceTracker.contains(token) else { return }

        _onceTracker.append(token)
        block()
    }

}

extension NSObject {
    public var rawPointer:UnsafeMutableRawPointer? {
        get {
            Unmanaged.passUnretained(self).toOpaque()
        }
    }
}

-3

Use el enfoque de constante de clase si está utilizando Swift 1.2 o superior y el enfoque de estructura anidada si necesita admitir versiones anteriores. Una exploración del patrón Singleton en Swift. Todos los enfoques siguientes admiten la inicialización diferida y la seguridad de subprocesos. El enfoque dispatch_once no funciona en Swift 3.0

Método A: constante de clase

class SingletonA {

    static let sharedInstance = SingletonA()

    init() {
        println("AAA");
    }

}

Método B: estructura anidada

class SingletonB {

    class var sharedInstance: SingletonB {
        struct Static {
            static let instance: SingletonB = SingletonB()
        }
        return Static.instance
    }

}

Enfoque C: dispatch_once

class SingletonC {

    class var sharedInstance: SingletonC {
        struct Static {
            static var onceToken: dispatch_once_t = 0
            static var instance: SingletonC? = nil
        }
        dispatch_once(&Static.onceToken) {
            Static.instance = SingletonC()
        }
        return Static.instance!
    }
}

1
La pregunta se hizo específicamente sobre una solución para Swift 3.
thesummersign
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.