wake-up-neo.com

dispatch_sync vs. dispatch_async in der Hauptwarteschlange

Nehmen Sie mit, es wird einige Erklärungen erfordern. Ich habe eine Funktion, die wie folgt aussieht.

Kontext: "aProject" ist eine Core Data-Entität namens LPProject mit einem Array namens 'memberFiles', das Instanzen einer anderen Core Data-Entität namens LPFile enthält. Jede LPFile stellt eine Datei auf der Festplatte dar. Wir möchten jede dieser Dateien öffnen und ihren Text analysieren, um nach @ import-Anweisungen zu suchen, die auf ANDERE Dateien verweisen. Wenn wir @import-Anweisungen finden, möchten wir die Datei finden, auf die sie zeigen, und diese Datei dann mit dieser verknüpfen, indem wir der Hauptdatenentität, die die erste Datei darstellt, eine Beziehung hinzufügen. Da all dies bei großen Dateien einige Zeit in Anspruch nehmen kann, werden wir dies mit GCD abseits des Hauptthreads tun.

- (void) establishImportLinksForFilesInProject:(LPProject *)aProject {
    dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
     for (LPFile *fileToCheck in aProject.memberFiles) {
         if (//Some condition is met) {
            dispatch_async(taskQ, ^{
                // Here, we do the scanning for @import statements. 
                // When we find a valid one, we put the whole path to the imported file into an array called 'verifiedImports'. 

                // go back to the main thread and update the model (Core Data is not thread-safe.)
                dispatch_sync(dispatch_get_main_queue(), ^{

                    NSLog(@"Got to main thread.");

                    for (NSString *import in verifiedImports) {  
                            // Add the relationship to Core Data LPFile entity.
                    }
                });//end block
            });//end block
        }
    }
}

Hier wird es merkwürdig:

Dieser Code funktioniert, aber ich sehe ein merkwürdiges Problem. Wenn ich es auf einem LPProject mit wenigen Dateien (ungefähr 20) ausführe, läuft es perfekt. Wenn ich es jedoch auf einem LPProject mit mehr Dateien (z. B. 60-70) ausführe, wird NICHT ordnungsgemäß ausgeführt. Wir kehren nie zum Haupt-Thread zurück, die Funktion NSLog(@"got to main thread"); wird nie angezeigt und die App bleibt hängen. ABER (und hier wird es WIRKLICH komisch) --- Wenn ich den Code für das kleine Projekt zuerst und dann für das große Projekt ausführe, funktioniert alles perfekt. NUR, wenn ich den Code für das große Projekt zuerst ausführe, wird der Fehler angezeigt.

Und hier ist der Kicker, wenn ich die zweite Versandlinie so ändere:

dispatch_async(dispatch_get_main_queue(), ^{

(Verwenden Sie also async anstelle von sync, um den Block an die Hauptwarteschlange zu senden.) Alles funktioniert die ganze Zeit. Perfekt. Unabhängig von der Anzahl der Dateien in einem Projekt!

Ich kann dieses Verhalten nicht erklären. Jede Hilfe oder Tipps, was als nächstes zu testen ist, wären wir dankbar.

52
Bryan

Dies ist ein häufiges Problem im Zusammenhang mit Festplatten-E/A und GCD. Grundsätzlich erzeugt GCD wahrscheinlich einen Thread für jede Datei, und zu einem bestimmten Zeitpunkt sind zu viele Threads für das System vorhanden, um in angemessener Zeit gewartet zu werden.

Jedes Mal, wenn Sie dispatch_async () aufrufen und in diesem Block versuchen, auf eine beliebige E/A zuzugreifen (es sieht beispielsweise so aus, als würden Sie hier einige Dateien lesen), wird der Thread, in dem dieser Codeblock ausgeführt wird, wahrscheinlich blockiert (wird vom Betriebssystem angehalten), während darauf gewartet wird, dass die Daten aus dem Dateisystem gelesen werden. Wenn GCD erkennt, dass einer seiner Arbeitsthreads für E/A blockiert ist und Sie immer noch mehr Arbeit gleichzeitig ausführen möchten, wird nur ein neuer Arbeitsthread erstellt. Wenn Sie also versuchen, 50 Dateien in einer gleichzeitigen Warteschlange zu öffnen, führt dies wahrscheinlich dazu, dass GCD ~ 50 Threads erzeugt.

Dies sind zu viele Threads, als dass das System einen sinnvollen Dienst leisten könnte, und am Ende wird der Haupt-Thread für die CPU ausgehungert.

Sie können dies beheben, indem Sie eine serielle Warteschlange anstelle einer gleichzeitigen Warteschlange für Ihre dateibasierten Vorgänge verwenden. Es ist leicht zu machen. Sie möchten eine serielle Warteschlange erstellen und als Ivar in Ihrem Objekt speichern, damit nicht mehrere serielle Warteschlangen erstellt werden. Also entferne diesen Anruf:

dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

Fügen Sie dies in Ihre init-Methode ein:

taskQ = dispatch_queue_create("com.yourcompany.yourMeaningfulLabel", DISPATCH_QUEUE_SERIAL);

Fügen Sie dies in Ihre Dealloc-Methode ein:

dispatch_release(taskQ);

Und füge dies als ivar in deine Klassendeklaration ein:

dispatch_queue_t taskQ;

53
Ryan

Ich glaube, Ryan ist auf dem richtigen Weg: Es werden einfach zu viele Threads erzeugt, wenn ein Projekt 1.500 Dateien enthält (die Menge, mit der ich mich zum Testen entschieden habe).

Also habe ich den obigen Code überarbeitet, um so zu funktionieren:

- (void) establishImportLinksForFilesInProject:(LPProject *)aProject
{
        dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

     dispatch_async(taskQ, 
     ^{

     // Create a new Core Data Context on this thread using the same persistent data store    
     // as the main thread. Pass the objectID of aProject to access the managedObject
     // for that project on this thread's context:

     NSManagedObjectID *projectID = [aProject objectID];

     for (LPFile *fileToCheck in [backgroundContext objectWithID:projectID] memberFiles])
     {
        if (//Some condition is met)
        {
                // Here, we do the scanning for @import statements. 
                // When we find a valid one, we put the whole path to the 
                // imported file into an array called 'verifiedImports'. 

                // Pass this ID to main thread in dispatch call below to access the same
                // file in the main thread's context
                NSManagedObjectID *fileID = [fileToCheck objectID];


                // go back to the main thread and update the model 
                // (Core Data is not thread-safe.)
                dispatch_async(dispatch_get_main_queue(), 
                ^{
                    for (NSString *import in verifiedImports)
                    {  
                       LPFile *targetFile = [mainContext objectWithID:fileID];
                       // Add the relationship to targetFile. 
                    }
                 });//end block
         }
    }
    // Easy way to tell when we're done processing all files.
    // Could add a dispatch_async(main_queue) call here to do something like UI updates, etc

    });//end block
    }

Im Grunde spawnen wir jetzt einen Thread, der alle Dateien anstelle von einem Thread pro Datei liest. Es stellt sich außerdem heraus, dass das Aufrufen von dispatch_async () in der main_queue der richtige Ansatz ist: Der Worker-Thread sendet diesen Block an den Haupt-Thread und wartet NICHT darauf, dass er zurückkehrt, bevor die nächste Datei gescannt wird.

Diese Implementierung richtet im Wesentlichen eine "serielle" Warteschlange ein, wie von Ryan vorgeschlagen (die for-Schleife ist der serielle Teil davon), aber mit einem Vorteil: Wenn die for-Schleife endet, sind wir mit der Verarbeitung aller Dateien fertig und können einfach eine stecken dispatch_async (main_queue) blockiert, um zu tun, was wir wollen. Es ist eine sehr gute Möglichkeit, um festzustellen, wann die gleichzeitige Verarbeitung abgeschlossen ist und in meiner alten Version nicht vorhanden war.

Der Nachteil hierbei ist, dass es etwas komplizierter ist, mit Core Data auf mehreren Threads zu arbeiten. Dieser Ansatz scheint jedoch für Projekte mit 5.000 Dateien kugelsicher zu sein (dies ist der höchste, den ich getestet habe).

5
Bryan

Ich denke, es ist einfacher mit Diagramm zu verstehen:

Für die Situation beschrieb der Autor:

| taskQ | *********** start |

| dispatch_1 *********** | ---------

| dispatch_2 ************* | ---------

.

| dispatch_n *************************** | ----------

| main queue (sync) | ** Startet den Versand an main |

************************* | --dispatch_1-- | --dispatch_2-- | --dispatch3-- | ****** *********************** | --dispatch_n |,

dadurch ist die Synchronisierungs-Hauptwarteschlange so voll, dass die Aufgabe endgültig fehlschlägt.

0
Damon Yuan