web-dev-qa-db-de.com

Warum können wir für die aktuelle Warteschlange kein dispatch_sync verwenden?

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?

57

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.

51
lawicko

dispatch_sync macht zwei Dinge:

  1. schlange einen Block
  2. blockiert den aktuellen Thread, bis der Block beendet ist

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:

  1. dispatch_sync stellt den Block in die Hauptwarteschlange.
  2. dispatch_sync blockiert den Thread der Hauptwarteschlange, bis die Ausführung des Blocks abgeschlossen ist.
  3. 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:

  • Ich erstelle eine 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).
  • Ich kann die Warteschlangen meiner Mitarbeiter zu jedem Zeitpunkt füllen. Um zu wissen, ob sie gefüllt sind oder nicht, beschrifte ich funnelQ mit dem Wort "Trichter".
  • Die Straße runter ich 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).

69
Jano

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.

14
Mecki

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.

6
Chris Suter

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.

4
newacct

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.

2
arango_86