Ich stieß auf ein Szenario, in dem ich einen Delegate-Callback hatte, der entweder im Haupt-Thread oder in einem anderen Thread auftreten konnte, und ich wusste erst zur Laufzeit, welche (mit StoreKit.framework
).
Ich hatte auch UI-Code, den ich in diesem Rückruf aktualisieren musste, der vor der Ausführung der Funktion erfolgen musste. Mein erster Gedanke war also, eine Funktion wie die folgende zu haben:
-(void) someDelegateCallback:(id) sender
{
dispatch_sync(dispatch_get_main_queue(), ^{
// ui update code here
});
// code here that depends upon the UI getting updated
}
Das funktioniert gut, wenn es auf dem Hintergrund-Thread ausgeführt wird. Wenn das Programm auf dem Hauptthread ausgeführt wird, kommt es jedoch zu einem Deadlock.
Das allein scheint mir interessant zu sein, wenn ich die Dokumente für dispatch_sync
Richtig lese, dann würde ich erwarten, dass es den Block direkt ausführt und sich nicht darum kümmert, ihn wie gesagt in den Runloop einzuplanen hier :
Als Optimierung ruft diese Funktion den Block auf dem aktuellen Thread auf, wenn dies möglich ist.
Aber das ist keine allzu große Sache, es bedeutet einfach ein bisschen mehr Tippen, was mich zu diesem Ansatz führt:
-(void) someDelegateCallBack:(id) sender
{
dispatch_block_t onMain = ^{
// update UI code here
};
if (dispatch_get_current_queue() == dispatch_get_main_queue())
onMain();
else
dispatch_sync(dispatch_get_main_queue(), onMain);
}
Dies scheint jedoch ein bisschen rückwärts zu sein. War dies ein Fehler bei der Erstellung von GCD, oder fehlt etwas in den Dokumenten?
Ich fand das in der Dokumentation (letztes Kapitel) :
Rufen Sie die Funktion dispatch_sync nicht von einer Task aus auf, die in derselben Warteschlange ausgeführt wird, die Sie an Ihren Funktionsaufruf übergeben haben. Andernfalls wird die Warteschlange blockiert. Wenn Sie in die aktuelle Warteschlange senden müssen, verwenden Sie dazu asynchron die Funktion dispatch_async.
Außerdem bin ich dem von Ihnen angegebenen Link gefolgt und habe in der Beschreibung von dispatch_sync Folgendes gelesen:
Das Aufrufen dieser Funktion und das Zielen auf die aktuelle Warteschlange führt zu einem Deadlock.
Ich glaube nicht, dass es ein Problem mit GCD ist. Ich denke, der einzig sinnvolle Ansatz ist der, den Sie erfunden haben, nachdem Sie das Problem entdeckt haben.
dispatch_sync
macht zwei Dinge:
Da der Haupt-Thread eine serielle Warteschlange ist (was bedeutet, dass nur ein Thread verwendet wird), lautet die folgende Anweisung:
dispatch_sync(dispatch_get_main_queue(), ^(){/*...*/});
wird die folgenden Ereignisse verursachen:
dispatch_sync
stellt den Block in die Hauptwarteschlange.dispatch_sync
blockiert den Thread der Hauptwarteschlange, bis die Ausführung des Blocks abgeschlossen ist.dispatch_sync
wartet ewig, da der Thread, in dem der Block ausgeführt werden soll, blockiert ist.Der Schlüssel zum Verständnis ist, dass dispatch_sync
führt keine Blöcke aus, sondern stellt sie nur in die Warteschlange. Die Ausführung erfolgt bei einer zukünftigen Iteration der Run-Schleife.
Der folgende Ansatz:
if (queueA == dispatch_get_current_queue()){
block();
} else {
dispatch_sync(queueA,block);
}
ist völlig in Ordnung, aber beachten Sie, dass es Sie nicht vor komplexen Szenarien mit einer Hierarchie von Warteschlangen schützt. In diesem Fall unterscheidet sich die aktuelle Warteschlange möglicherweise von einer zuvor blockierten Warteschlange, an die Sie Ihren Block senden möchten. Beispiel:
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
// dispatch_get_current_queue() is B, but A is blocked,
// so a dispatch_sync(A,b) will deadlock.
dispatch_sync(queueA, ^{
// some task
});
});
});
Lesen/Schreiben Sie in komplexen Fällen Schlüsselwertdaten in die Dispatch-Warteschlange:
dispatch_queue_t workerQ = dispatch_queue_create("com.meh.sometask", NULL);
dispatch_queue_t funnelQ = dispatch_queue_create("com.meh.funnel", NULL);
dispatch_set_target_queue(workerQ,funnelQ);
static int kKey;
// saves string "funnel" in funnelQ
CFStringRef tag = CFSTR("funnel");
dispatch_queue_set_specific(funnelQ,
&kKey,
(void*)tag,
(dispatch_function_t)CFRelease);
dispatch_sync(workerQ, ^{
// is funnelQ in the hierarchy of workerQ?
CFStringRef tag = dispatch_get_specific(&kKey);
if (tag){
dispatch_sync(funnelQ, ^{
// some task
});
} else {
// some task
}
});
Erläuterung:
workerQ
Warteschlange, die auf eine funnelQ
Warteschlange verweist. In echtem Code ist dies nützlich, wenn Sie mehrere "Worker" -Warteschlangen haben und alle gleichzeitig fortsetzen/anhalten möchten (dies wird durch Wiederaufnahme/Aktualisierung der Zielwarteschlange funnelQ
erreicht).funnelQ
mit dem Wort "Trichter".dispatch_sync
etwas zu workerQ
und aus irgendeinem Grund möchte ich dispatch_sync
zu funnelQ
, aber um ein dispatch_sync zur aktuellen Warteschlange zu vermeiden, überprüfe ich das Tag und handle dementsprechend. Da der get die Hierarchie durchläuft, wird der Wert nicht in workerQ
, sondern in funnelQ
gefunden. Auf diese Weise können Sie feststellen, ob in einer Warteschlange in der Hierarchie der Wert gespeichert wurde. Und deshalb, um ein dispatch_sync zur aktuellen Warteschlange zu verhindern.Wenn Sie sich über die Funktionen zum Lesen/Schreiben von Kontextdaten wundern, gibt es drei:
dispatch_queue_set_specific
: In eine Warteschlange schreiben.dispatch_queue_get_specific
: Aus einer Warteschlange lesen.dispatch_get_specific
: Komfortfunktion zum Lesen aus der aktuellen Warteschlange.Der Schlüssel wird durch einen Zeiger verglichen und niemals dereferenziert. Der letzte Parameter im Setter ist ein Destruktor, der den Schlüssel freigibt.
Wenn Sie sich fragen, ob Sie eine Warteschlange auf eine andere verweisen möchten, bedeutet dies genau das. Beispielsweise kann ich eine Warteschlange A auf die Hauptwarteschlange verweisen, wodurch alle Blöcke in der Warteschlange A in der Hauptwarteschlange ausgeführt werden (normalerweise wird dies für Benutzeroberflächenaktualisierungen durchgeführt).
Ich weiß, woher deine Verwirrung kommt:
Als Optimierung ruft diese Funktion den Block auf dem aktuellen Thread auf, wenn dies möglich ist.
Vorsicht, es steht aktueller Thread .
Thread! = Queue
Eine Warteschlange besitzt keinen Thread und ein Thread ist nicht an eine Warteschlange gebunden. Es gibt Threads und Warteschlangen. Wenn eine Warteschlange einen Block ausführen möchte, benötigt sie einen Thread, der jedoch nicht immer derselbe sein muss. Es wird nur ein Thread benötigt (dies kann jedes Mal ein anderer sein), und wenn die Ausführung von Blöcken abgeschlossen ist (im Moment), kann derselbe Thread jetzt von einer anderen Warteschlange verwendet werden.
Die Optimierung, um die es in diesem Satz geht, bezieht sich auf Threads, nicht auf Warteschlangen. Z.B. Angenommen, Sie haben zwei serielle Warteschlangen, QueueA
und QueueB
, und jetzt gehen Sie wie folgt vor:
dispatch_async(QueueA, ^{
someFunctionA(...);
dispatch_sync(QueueB, ^{
someFunctionB(...);
});
});
Wenn QueueA
den Block ausführt, besitzt er vorübergehend einen Thread, einen beliebigen Thread. someFunctionA(...)
wird in diesem Thread ausgeführt. Jetzt kann QueueA
während des synchronen Versands nichts anderes tun, sondern muss warten, bis der Versand abgeschlossen ist. QueueB
benötigt andererseits auch einen Thread, um seinen Block auszuführen und someFunctionB(...)
auszuführen. Entweder setzt QueueA
seinen Thread vorübergehend aus und QueueB
verwendet einen anderen Thread, um den Block auszuführen, oder QueueA
übergibt seinen Thread an QueueB
(nachdem er gewonnen hat) wird sowieso nicht benötigt, bis der synchrone Versand beendet ist) und QueueB
verwendet direkt den aktuellen Thread von QueueA
.
Unnötig zu erwähnen, dass die letzte Option viel schneller ist, da kein Threadwechsel erforderlich ist. Und this ist die Optimierung, von der der Satz spricht. Eine dispatch_sync()
in einer anderen Warteschlange kann daher nicht immer zu einem Threadwechsel führen (andere Warteschlange, möglicherweise derselbe Thread).
Aber eine dispatch_sync()
kann immer noch nicht mit derselben Warteschlange passieren (gleicher Thread, ja, dieselbe Warteschlange, nein). Das liegt daran, dass eine Warteschlange Block für Block ausgeführt wird. Wenn sie derzeit einen Block ausführt, führt sie keinen weiteren aus, bis die derzeit ausgeführte Ausführung abgeschlossen ist. Also führt es BlockA
aus und BlockA
führt eine dispatch_sync()
von BlockB
in derselben Warteschlange aus. Die Warteschlange wird nicht ausgeführt BlockB
, solange sie noch ausgeführt wird BlockA
, aber die Ausführung von BlockA
wird erst fortgesetzt, wenn BlockB
ausgeführt wurde. Sehen Sie das Problem? Es ist eine klassische Sackgasse.
In der Dokumentation wird eindeutig angegeben, dass das Übergeben der aktuellen Warteschlange zu einem Deadlock führt.
Jetzt sagen sie nicht, warum sie die Dinge so entworfen haben (außer, dass es zusätzlichen Code erfordert, damit sie funktionieren), aber ich vermute, der Grund für diese Vorgehensweise liegt darin, dass in diesem speziellen Fall Blöcke "springen". die Warteschlange, dh in normalen Fällen wird Ihr Block ausgeführt, nachdem alle anderen Blöcke in der Warteschlange ausgeführt wurden, in diesem Fall jedoch zuvor.
Dieses Problem tritt auf, wenn Sie versuchen, GCD als Mechanismus zum gegenseitigen Ausschluss zu verwenden, und dieser spezielle Fall entspricht der Verwendung eines rekursiven Mutex. Ich möchte nicht darüber streiten, ob es besser ist, GCD oder ein traditionelles API für gegenseitigen Ausschluss wie pthreads-Mutexe zu verwenden oder ob es eine gute Idee ist, rekursive Mutexe zu verwenden. Ich lasse andere darüber streiten, aber es gibt sicherlich eine Nachfrage danach, insbesondere wenn es sich um die Hauptwarteschlange handelt, mit der Sie es zu tun haben.
Persönlich denke ich, dass dispatch_sync nützlicher wäre, wenn es dies unterstützen würde oder wenn es eine andere Funktion gäbe, die das alternative Verhalten ermöglicht. Ich möchte andere, die dies meinen, dazu auffordern, einen Fehlerbericht mit Apple (wie ich getan habe, ID: 12668073) einzureichen.
Sie können Ihre eigene Funktion schreiben, um dasselbe zu tun, aber es ist ein bisschen ein Hack:
// Like dispatch_sync but works on current queue
static inline void dispatch_synchronized (dispatch_queue_t queue,
dispatch_block_t block)
{
dispatch_queue_set_specific (queue, queue, (void *)1, NULL);
if (dispatch_get_specific (queue))
block ();
else
dispatch_sync (queue, block);
}
N.B. Früher hatte ich ein Beispiel, das dispatch_get_current_queue () verwendete, das jetzt jedoch veraltet ist.
Beide dispatch_async
und dispatch_sync
ausführen Schieben Sie ihre Aktion in die gewünschte Warteschlange. Die Aktion findet nicht sofort statt. Dies geschieht bei einer späteren Iteration der Ausführungsschleife der Warteschlange. Der Unterschied zwischen dispatch_async
und dispatch_sync
ist dass dispatch_sync
blockiert die aktuelle Warteschlange, bis die Aktion beendet ist.
Überlegen Sie, was passiert, wenn Sie in der aktuellen Warteschlange etwas asynchron ausführen. Wieder geschieht es nicht sofort; Es wird in eine FIFO Warteschlange gestellt und muss warten, bis die aktuelle Iteration der Ausführungsschleife abgeschlossen ist (und möglicherweise auch warten, bis andere Aktionen in der Warteschlange ausgeführt wurden, bevor Sie dies tun neue Aktion am).
Wenn Sie eine Aktion asynchron in der aktuellen Warteschlange ausführen, können Sie jetzt fragen, warum Sie die Funktion nicht immer direkt aufrufen, anstatt auf eine spätere Zeit zu warten. Die Antwort ist, dass es einen großen Unterschied zwischen den beiden gibt. Häufig müssen Sie eine Aktion ausführen, diese muss jedoch ausgeführt werden nach welche Nebeneffekte auch immer von Funktionen auf dem Stapel in der aktuellen Iteration der Ausführungsschleife ausgeführt werden. oder Sie müssen Ihre Aktion nach einer Animationsaktion ausführen, die bereits in der Ausführungsschleife geplant ist, usw. Aus diesem Grund wird der Code häufig angezeigt [obj performSelector:selector withObject:foo afterDelay:0]
(ja, es ist anders als [obj performSelector:selector withObject:foo]
).
Wie wir schon sagten, dispatch_sync
ist das gleiche wie dispatch_async
, außer dass es blockiert, bis die Aktion abgeschlossen ist. Es ist also offensichtlich, warum es zu einem Deadlock kommen würde - der Block kann erst ausgeführt werden, nachdem die aktuelle Iteration der Run-Schleife abgeschlossen ist. Aber wir warten darauf, dass es zu Ende ist, bevor wir weitermachen.
Theoretisch wäre es möglich, einen Sonderfall für dispatch_sync
für den aktuellen Thread, um ihn sofort auszuführen. (Ein solcher Sonderfall besteht für performSelector:onThread:withObject:waitUntilDone:
, wenn der Thread der aktuelle Thread ist und waitUntilDone:
ist JA, es führt es sofort aus.) Allerdings habe ich Apple) entschieden, dass es besser ist, hier ein konsistentes Verhalten zu haben, unabhängig von der Warteschlange.
Gefunden aus der folgenden Dokumentation. https://developer.Apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html#//Apple_ref/c/func/dispatch_sync
Im Gegensatz zu dispatch_async kehrt die Funktion " dispatch_sync" erst nach Beendigung des Blocks zurück. Das Aufrufen dieser Funktion und das Zielen auf die aktuelle Warteschlange führt zu einem Deadlock.
Im Gegensatz zu dispatch_async wird für die Zielwarteschlange kein Retain durchgeführt. Da Aufrufe dieser Funktion synchron sind, ist " leiht" die Referenz des Aufrufers. Außerdem wird kein Block_copy für den Block ausgeführt.
Als Optimierung ruft diese Funktion den Block auf dem aktuellen Thread auf, wenn dies möglich ist.