2010-03-10 13 views
7

ich quer durch das auf dem Mike Ash „Pflege und Fütterung von Singletons“ kam und war ein wenig von seinem Kommentar puzzeled:Mike Ash Singleton: Platzierung @synchronized

Dieser Code ist etwas langsam, aber. Eine Sperre ist etwas teuer. Making es schmerzhafter ist die Tatsache , dass die überwiegende Mehrheit der Zeit, das Schloss ist sinnlos. Die Sperre wird nur benötigt, wenn foo null ist, was grundsätzlich nur einmal passiert. Nachdem der Singleton initialisiert wurde, ist die Notwendigkeit für das Schloss weg, aber das Schloss selbst bleibt .

+(id)sharedFoo { 
    static Foo *foo = nil; 
    @synchronized([Foo class]) { 
     if(!foo) foo = [[self alloc] init]; 
    } 
    return foo; 
} 

Meine Frage ist, und es besteht kein Zweifel ein guter Grund dafür, aber warum können Sie nicht schreiben (siehe unten), um die Sperre zu begrenzen, wenn foo ist gleich Null?

+(id)sharedFoo { 
    static Foo *foo = nil; 
    if(!foo) { 
     @synchronized([Foo class]) { 
      foo = [[self alloc] init]; 
     } 
    } 
    return foo; 
} 

prost gary

+1

Ah ok, so dass im Grunde brauchen Sie einen Scheck in dem @synchronize Block? – fuzzygoat

+0

Das ist der springende Punkt von @synchronized: Erlaube jeweils einem Thread die Überprüfung. –

+0

Versuchen Sie stattdessen dispatch_once(): http://StackOverflow.com/q/5720029/290295 – ctpenrose

Antwort

18

Weil dann der Test zu einer Race-Bedingung unterliegt. Zwei verschiedene Threads könnten unabhängig testen, dass foonil ist, und dann (sequenziell) separate Instanzen erstellen. Dies kann in Ihrer modifizierten Version vorkommen, wenn ein Thread den Test ausführt, während sich die andere noch innerhalb von +[Foo alloc] oder -[Foo init] befindet, aber foo noch nicht festgelegt ist.

Übrigens würde ich es nicht so machen. Sehen Sie sich die Funktion dispatch_once() an, mit der Sie garantieren können, dass ein Block während der Laufzeit Ihrer App nur einmal ausgeführt wird (vorausgesetzt, Sie haben GCD auf der Plattform, auf die Sie ausgerichtet sind).

+0

Das ist natürlich wahr. Aber wäre nicht die beste Lösung, zweimal zu testen (innerhalb ** und ** außerhalb des '@ synchronisierten'). Dann gäbe es keine Race-Condition oder Leistungsstrafe. –

+1

@Nikolai: Sag mir, es gibt eine Leistungsstrafe _after_ du hast Shark laufen lassen. :-) –

+0

@Graham: Es besteht kein Zweifel, dass die Leistung in der ursprünglichen Version schlecht ist, die immer die Sperre übernimmt. Ich hatte es in meinem Code * und ich habe Shark *;) laufen lassen. Auch Mike Ash wies darauf in seinem ursprünglichen Blogpost hin. –

1

In Ihrer Version könnte die Überprüfung für !foo gleichzeitig bei mehreren Threads auftreten, sodass zwei Threads in den Block alloc springen können. Einer wartet darauf, dass der andere beendet wird, bevor er eine andere Instanz zuweist.

1

Sie können optimieren, indem Sie nur die Sperre übernehmen, wenn foo == nil, aber danach müssen Sie erneut (innerhalb der @synchronized) testen, um gegen Wettlaufbedingungen zu schützen.

+ (id)sharedFoo { 
    static Foo *foo = nil; 
    if(!foo) { 
     @synchronized([Foo class]) { 
      if (!foo) // test again, in case 2 threads doing this at once 
       foo = [[self alloc] init]; 
     } 
    } 
    return foo; 
} 
+2

Siehe @mfazekas Antwort für warum das falsch ist. –

7

Dies wird double checked locking "optimization" genannt. Wie überall dokumentiert, ist dies nicht sicher. Selbst wenn es nicht durch eine Compiler-Optimierung besiegt wird, wird es die Art und Weise, wie Speicher auf modernen Maschinen arbeitet, besiegen, es sei denn, Sie verwenden eine Art Zaun/Barrieren.

Mike Ash also shows die richtige Lösung mit volatile und OSMemoryBarrier();.

Das Problem ist, dass, wenn ein Thread ausgeführt wird foo = [[self alloc] init]; es keine Garantie dafür gibt, dass, wenn ein anderer Thread sieht foo != 0 alle Speicher von init ausgeführt schreibt auch sichtbar.

Siehe auch DCL and C++ und DCL and java für weitere Details.

+0

+1 Danke, dass Sie das klar gemacht haben. Befehlsumordnung und Speicherzugriff außerhalb der Reihenfolge sind beides Konzepte, die den meisten Programmierern nicht bekannt sind. –

+3

dispatch_once ist die echte Lösung, nutzen Sie das einfach und beenden Sie das Hacken – slf

+0

Ich denke, dass SLF es hat. http://stackoverflow.com/q/5720029/290295 – ctpenrose

1

Der beste Weg, wenn Sie großer cenral Versand

+ (MySingleton*) instance { 
static dispatch_once_t _singletonPredicate; 
static MySingleton *_singleton = nil; 

dispatch_once(&_singletonPredicate, ^{ 
    _singleton = [[super allocWithZone:nil] init]; 
}); 

return _singleton 
} 
+ (id) allocWithZone:(NSZone *)zone { 
    return [self instance]; 
}