web-dev-qa-db-fra.com

dispatch_sync vs dispatch_async dans la file d'attente principale

Restez avec moi, cela va prendre quelques explications. J'ai une fonction qui ressemble à celle ci-dessous.

Contexte: "aProject" est une entité Core Data nommée LPProject avec un tableau nommé 'memberFiles' qui contient des instances d'une autre entité Core Data appelée LPFile. Chaque LPFile représente un fichier sur le disque et ce que nous voulons faire, c'est ouvrir chacun de ces fichiers et analyser son texte, à la recherche d'instructions @import qui pointent vers D'AUTRES fichiers. Si nous trouvons des instructions @import, nous voulons localiser le fichier vers lequel elles pointent, puis "lier" ce fichier à celui-ci en ajoutant une relation à l'entité de données principale qui représente le premier fichier. Étant donné que tout cela peut prendre un certain temps sur des fichiers volumineux, nous le ferons à partir du thread principal en utilisant GCD.

- (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
        }
    }
}

Maintenant, voici où les choses deviennent étranges:

Ce code fonctionne, mais je vois un problème étrange. Si je l'exécute sur un LPProject qui a quelques fichiers (environ 20), il fonctionne parfaitement. Cependant, si je l'exécute sur un LPProject qui a plus de fichiers (disons, 60-70), il ne PAS s'exécute correctement. Nous ne revenons jamais au thread principal, la NSLog(@"got to main thread"); n'apparaît jamais et l'application se bloque. MAIS, (et c'est là que les choses deviennent VRAIMENT bizarres) --- si j'exécute le code sur le petit projet EN PREMIER et PUIS l'exécute sur le grand projet, tout fonctionne parfaitement. C'est UNIQUEMENT lorsque j'exécute le code sur le grand projet en premier que le problème apparaît.

Et voici le kicker, si je change la deuxième ligne d'expédition en ceci:

dispatch_async(dispatch_get_main_queue(), ^{

(Autrement dit, utilisez async au lieu de sync pour envoyer le bloc dans la file d'attente principale), tout fonctionne tout le temps. À la perfection. Quel que soit le nombre de fichiers dans un projet!

Je n'arrive pas à expliquer ce comportement. Toute aide ou conseil sur ce que vous allez tester ensuite serait apprécié.

52
Bryan

Il s'agit d'un problème courant lié aux E/S disque et GCD. Fondamentalement, GCD génère probablement un thread pour chaque fichier, et à un certain point, vous avez trop de threads pour que le système puisse les entretenir dans un délai raisonnable.

Chaque fois que vous appelez dispatch_async () et dans ce bloc, vous essayez d'accéder à n'importe quelle E/S (par exemple, il semble que vous lisez des fichiers ici), il est probable que le thread dans lequel ce bloc de code s'exécute se bloque (interrompu par le système d'exploitation) pendant qu'il attend que les données soient lues à partir du système de fichiers. Le fonctionnement de GCD est tel que lorsqu'il voit qu'un de ses threads de travail est bloqué sur les E/S et que vous lui demandez toujours de faire plus de travail simultanément, il ne fera que générer un nouveau thread de travail. Ainsi, si vous essayez d'ouvrir 50 fichiers sur une file d'attente simultanée, il est probable que vous finissiez par faire apparaître GCD ~ 50 threads.

Il s'agit d'un nombre trop important de threads pour que le système puisse fonctionner correctement, et vous finissez par affamer votre thread principal pour le processeur.

La solution consiste à utiliser une file d'attente série au lieu d'une file d'attente simultanée pour effectuer vos opérations basées sur des fichiers. C'est facile à faire. Vous souhaiterez créer une file d'attente série et la stocker en tant qu'ivar dans votre objet afin de ne pas finir par créer plusieurs files d'attente série. Supprimez donc cet appel:

dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

Ajoutez ceci dans votre méthode init:

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

Ajoutez ceci dans votre méthode dealloc:

dispatch_release(taskQ);

Et ajoutez ceci comme un ivar dans votre déclaration de classe:

dispatch_queue_t taskQ;

53
Ryan

Je crois que Ryan est sur la bonne voie: il y a tout simplement trop de threads générés lorsqu'un projet a 1 500 fichiers (le montant avec lequel j'ai décidé de tester.)

J'ai donc refactorisé le code ci-dessus pour qu'il fonctionne comme ceci:

- (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
    }

Donc, fondamentalement, nous générons maintenant un thread qui lit tous les fichiers au lieu d'un thread par fichier. En outre, il s'avère que l'appel de dispatch_async () sur la file d'attente principale est la bonne approche: le thread de travail enverra ce bloc au thread principal et n'attendra PAS qu'il revienne avant de procéder à l'analyse du fichier suivant.

Cette implémentation met essentiellement en place une file d'attente "série" comme l'a suggéré Ryan (la boucle for en fait partie), mais avec un avantage: lorsque la boucle for se termine, nous avons fini de traiter tous les fichiers et nous pouvons simplement coller un dispatch_async (main_queue) s'y bloque pour faire ce que nous voulons. C'est une très belle façon de savoir quand la tâche de traitement simultané est terminée et qui n'existait pas dans mon ancienne version.

L'inconvénient ici est qu'il est un peu plus compliqué de travailler avec Core Data sur plusieurs threads. Mais cette approche semble être à l'épreuve des balles pour les projets avec 5 000 fichiers (ce qui est le plus élevé que j'ai testé.)

5
Bryan

Je pense que c'est plus facile à comprendre avec le diagramme:

Pour la situation, l'auteur a décrit:

| taskQ | *********** commencer |

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

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

.

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

| file d'attente principale (synchronisation) | ** commencez à envoyer à la main |

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

ce qui rend la file d'attente principale de synchronisation si occupée que la tâche échoue finalement.

0
Damon Yuan