dispatch_once dopo le modifiche all'API GCD di Swift 3


85

Qual è la nuova sintassi dispatch_oncein Swift dopo le modifiche apportate nella versione 3 della lingua? La vecchia versione era la seguente.

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

Queste sono le modifiche apportate a libdispatch .



Sulla base delle risposte stackoverflow.com/a/38311178/1648724~~V~~singular~~1st e stackoverflow.com/a/39983813/1648724 , ho creato un CocoaPod per fare questo: pod 'SwiftDispatchOnce', '~> 1.0'Acclamazioni. :]
JRG-Developer

Risposte:


70

Dal doc :

Spedizione
La funzione gratuita dispatch_once non è più disponibile in Swift. In Swift, puoi utilizzare valori globali o proprietà statiche inizializzati pigramente e ottenere le stesse garanzie di thread safety e call-once fornite da dispatch_once. Esempio:

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
Non è come se non sapessi che Swift cambierà rapidamente e dovresti correggere un sacco di codice rotto tra le versioni di Swift.
Abizern

2
il problema più grande sono i pod di terze parti che non sono sempre compatibili con Swift3.
Tinkerbell

4
Questo è il debito tecnico che accumuli quando introduci dipendenze di terze parti, @Tinkerbell. Amo Swift ma sono molto cauto nel portare dipendenze esterne che lo usano proprio per questo motivo.
Chris Wagner

17
dispatch_onceera chiaro. Questo, purtroppo, è brutto e confuso ..
Alexandre G

105

Sebbene l'utilizzo di variabili globali inizializzate pigre possa avere senso per alcune inizializzazioni una tantum, non ha senso per altri tipi. Ha molto senso usare variabili globali inizializzate pigre per cose come i singleton, non ha molto senso per cose come la protezione di una configurazione swizzle.

Ecco un'implementazione in stile Swift 3 di 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()
    }
}

Ecco un esempio di utilizzo:

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

o utilizzando un UUID

private let _onceToken = NSUUID().uuidString

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

Poiché attualmente siamo in un periodo di transizione da swift 2 a 3, ecco un esempio di implementazione di 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()
    }

}

Grazie mille per la soluzione. Stavo esattamente rimanendo intrappolato in una situazione di swizzle. Spero che il rapido team affronti questo caso d'uso.
salman140

2
Non dovresti assolutamente usare objc_sync_entere objc_sync_exitpiù.
smat88dd

1
E perché è così?
Tod Cunningham

1
Per le prestazioni dovresti usare un set invece di un array per _onceTrackers. Ciò migliora la complessità temporale da O (N) a O (1).
Werner Altewischer

2
Quindi scrivi una classe riutilizzabile assumendo che non venga riutilizzata molto :-) Se non richiede alcuno sforzo aggiuntivo per ridurre la complessità temporale da O (N) a O (1) dovresti sempre farlo IMHO.
Werner Altewischer

64

Espandendo la risposta di Tod Cunningham sopra, ho aggiunto un altro metodo che rende automaticamente il token da file, funzione e riga.

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

Quindi può essere più semplice chiamare:

DispatchQueue.once {
    setupUI()
}

e puoi ancora specificare un token se lo desideri:

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

Suppongo che potresti ottenere una collisione se hai lo stesso file in due moduli. Peccato che non ci sia#module


questo fa luce su cosa sta succedendo. Grazie.
Nyxee


Davvero aiutato Thanx
Manjunath C.Kadani

ma se ho capito bene, il tuo codice verrà eseguito solo una volta, anche se la classe viene rimossa e ricreata, giusto? è diverso dall'originale dispatch_once, se ricordo bene ... non è?
yonivav

1
@yonivav Sì, hai ragione. Per aggirare questo problema potresti anche passare nell'indirizzo di memoria dell'oggetto insieme a file, riga, ecc ... Rendi l'indirizzo parte del token.
VaporwareWolf

19

modificare

Risposta di @ Frizlab: non è garantito che questa soluzione sia thread-safe. Se questo è cruciale, dovrebbe essere utilizzata un'alternativa

La soluzione semplice è

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

    self.title = "Hello Lazy Guy"

    return
}()

usato come

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

1
Questo non aiuta affatto, perché una dichiarazione var lazy non può essere fatta in linea con il codice normale, deve essere in una definizione di struttura o classe. Ciò significa che il contenuto di dispatchOnce non può acquisire l'ambito circostante di un'istanza. Ad esempio, se dichiari una chiusura che non è ancora stata eseguita, non puoi quindi dichiarare la struttura all'interno di quella chiusura e fare in modo che il contenuto della lazy var sia un'altra chiusura che cattura i vars dalla chiusura circostante ...
CommaToast

3
Downvoted perché questo codice ha sicuramente non la stessa semantica dispatch_once. dispatch_once garantisce che il codice venga eseguito esattamente una volta, qualunque sia il thread da cui lo chiami . Le variabili pigre hanno un comportamento indefinito in un ambiente multi-thread.
Frizlab

in questa soluzione init block chiamerà due volte in alcuni casi
sacro

8

Puoi ancora usarlo se aggiungi un'intestazione di bridging:

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

Poi da .mqualche parte:

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

Ora dovresti essere in grado di utilizzare mxcl_dispatch_onceda Swift.

Principalmente dovresti usare ciò che Apple suggerisce invece, ma ho avuto alcuni usi legittimi in cui avevo bisogno di dispatch_onceun singolo token in due funzioni e non è coperto da ciò che Apple fornisce invece.


8

Puoi dichiarare una funzione variabile di primo livello come questa:

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

quindi chiamalo ovunque:

doOnce()

1
I vars pigri hanno come ambito la classe, quindi questo non si comporterà assolutamente come dispatch_once. Verrà eseguito una volta per istanza della classe sottostante. O spostalo fuori dalla classe [private var doOnce: () -> () = {}] o contrassegnalo come static [static private var doOnce: () -> () = {}]
Eli Burke,

1
Assolutamente corretto! Grazie. Nella maggior parte dei casi è necessaria un'azione una volta per istanza.
Bogdan Novikov

2
Questa è davvero un'ottima soluzione! Elegante, breve e chiaro
Ben Leggiero

6

Swift 3: per coloro a cui piacciono le classi (o strutture) riutilizzabili:

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

Utilizzo:

class MyViewController: UIViewController {

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

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

}

Aggiornamento (28 aprile 2017): OSSpinLocksostituito con os_unfair_lockavvisi di ritiro scaduto in 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)
   }
}

Ricevo un messaggio che OSSSpinLock è deprecato in iOS 10.0
markhorrocks

2
Grazie! Codice di esempio aggiornato. OSSpinLocksostituito con os_unfair_lock. BTW: Ecco un buon video WWDC su Concurrent Programming: developer.apple.com/videos/play/wwdc2016/720
Vlad

0

Miglioramento sopra le risposte ottengo risultato:

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

Aggiungi un esempio su come usarlo.
Satyam

-3

Utilizza l'approccio della costante di classe se stai utilizzando Swift 1.2 o versioni successive e l'approccio della struttura nidificata se devi supportare le versioni precedenti. Un'esplorazione del pattern Singleton in Swift. Tutti gli approcci seguenti supportano l'inizializzazione lenta e la sicurezza dei thread. L'approccio dispatch_once non funziona in Swift 3.0

Approccio A: costante di classe

class SingletonA {

    static let sharedInstance = SingletonA()

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

}

Approccio B: struttura annidata

class SingletonB {

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

}

Approccio 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 domanda specificamente posta su una soluzione per Swift 3.
segno dell'estate
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.