Dlaczego Apple zaleca stosowanie dispatch_once do implementacji wzorca singletonu pod ARC?

305

Jaki jest dokładny powód użycia dispatch_once we współużytkowanym instancie dla singletona pod ARC?

+ (MyClass *)sharedInstance
{
    //  Static local predicate must be initialized to 0
    static MyClass *sharedInstance = nil;
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[MyClass alloc] init];
        // Do any other initialisation stuff here
    });
    return sharedInstance;
}

Czy nie jest złym pomysłem asynchroniczne tworzenie singletonu w tle? Mam na myśli to, co się stanie, jeśli zażądam udostępnionego wystąpienia i natychmiast na nim polegam, ale dispatch_once trwa do świąt Bożego Narodzenia, aby utworzyć mój obiekt? Nie wraca natychmiast, prawda? Przynajmniej wydaje się, że o to właśnie chodzi w Grand Central Dispatch.

Dlaczego to robią?

Dumny członek
źródło
Note: static and global variables default to zero.
ikkentim

Odpowiedzi:

418

dispatch_once()jest absolutnie synchroniczny. Nie wszystkie metody GCD działają asynchronicznie (przypadek dispatch_sync()jest synchroniczny). Zastosowanie dispatch_once()zastępuje następujący idiom:

+ (MyClass *)sharedInstance {
    static MyClass *sharedInstance;
    @synchronized(self) {
        if (sharedInstance == nil) {
            sharedInstance = [[MyClass alloc] init];
        }
    }
    return sharedInstance;
}

Zaletą dispatch_once()tego jest to, że jest szybszy. Jest również semantycznie czystszy, ponieważ chroni również przed wieloma wątkami wykonującymi alokację init z SharedInstance - jeśli wszystkie próbują w tym samym czasie. Nie pozwoli na utworzenie dwóch instancji. Cała idea dispatch_once()polega na „wykonywaniu czegoś raz i tylko raz”, co właśnie robimy.

Lily Ballard
źródło
4
Ze względu na ten argument muszę zauważyć, że dokumentacja nie mówi, że jest wykonywana synchronicznie. Mówi tylko, że wiele równoczesnych wywołań zostanie serializowanych.
ekspert
5
Twierdzisz, że jest szybszy - o ile szybciej? Nie mam powodu sądzić, że nie mówisz prawdy, ale chciałbym zobaczyć prosty punkt odniesienia.
Joshua Gross
29
Właśnie zrobiłem prosty test porównawczy (na iPhonie 5) i wygląda na to, że dispatch_once jest około 2x szybszy niż @synchronized.
Joshua Gross
3
@ReneDohan: Jeśli masz 100% pewności, że nikt nigdy nie wywołuje tej metody z innego wątku, to działa. Ale korzystanie z niego dispatch_once()jest bardzo proste (zwłaszcza, że ​​Xcode będzie nawet automatycznie uzupełniał go do pełnego fragmentu kodu) i oznacza, że ​​nigdy nie musisz zastanawiać się, czy metoda musi być bezpieczna dla wątków.
Lily Ballard,
1
@siuying: Właściwie to nieprawda. Po pierwsze, wszystko, co się +initializedzieje, dzieje się przed dotknięciem klasy, nawet jeśli nie próbujesz jeszcze utworzyć współużytkowanej instancji. Ogólnie rzecz biorąc, leniwa inicjalizacja (tworzenie czegoś tylko w razie potrzeby) jest lepsza. Po drugie, nawet twoje twierdzenie o wydajności nie jest prawdziwe. dispatch_once()zajmuje prawie dokładnie taką samą ilość czasu jak mówią if (self == [MyClass class])w +initialize. Jeśli masz już +initialize, to tak, tworzenie współużytkowanej instancji jest szybsze, ale większość klas nie.
Lily Ballard
41

Ponieważ będzie działać tylko raz. Więc jeśli spróbujesz uzyskać do niego dostęp dwa razy z różnych wątków, nie spowoduje to problemu.

Mike Ash ma pełny opis w swoim wpisie na blogu Care and Feeding of Singletons .

Nie wszystkie bloki GCD są uruchamiane asynchronicznie.

Abizern
źródło
Lily's jest lepszą odpowiedzią, ale zostawiam moją, aby zachować link do posta Mike'a Ash'a.
Abizern