dispatch_sync vs. dispatch_async dans la queue principale

Gardez avec moi, cela va prendre des 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 fichier LPFile représente un fichier sur le disque et ce que nous voulons faire est d’ouvrir chacun de ces fichiers et d’parsingr son texte, en recherchant les instructions @import qui pointent sur les fichiers AUTRES. Si nous trouvons des instructions @import, nous souhaitons localiser le fichier vers lequel elles sont pointées, 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 les gros fichiers, nous le ferons 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 (NSSsortingng *import in verifiedImports) { // Add the relationship to Core Data LPFile entity. } });//end block });//end block } } } 

Maintenant, voici où les choses deviennent bizarres:

Ce code fonctionne, mais je constate un étrange problème. Si je l’exécute sur un LPProject contenant quelques fichiers (environ 20), il fonctionne parfaitement. Toutefois, si je l’exécute sur un LPProject contenant plus de fichiers (par exemple, 60-70), il ne s’exécute pas correctement. Nous ne revenons jamais au thread principal, le NSLog(@"got to main thread"); n’apparaît jamais et l’application se bloque. MAIS, (et c’est là que les choses deviennent vraiment étranges) — si je lance le code sur le petit projet FIRST et que je le lance sur le grand projet, tout fonctionne parfaitement. C’est UNIQUEMENT lorsque je lance 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 à ceci:

 dispatch_async(dispatch_get_main_queue(), ^{ 

(Autrement dit, utilisez async au lieu de sync pour envoyer le bloc dans la queue principale), tout fonctionne en permanence. À la perfection. Quel que soit le nombre de fichiers dans un projet!

Je suis incapable d’expliquer ce comportement. Toute aide ou conseil sur ce que vous devez tester ensuite serait apprécié.

Ceci est un problème commun lié aux E / S de disque et à GCD. Fondamentalement, GCD génère probablement un thread pour chaque fichier, et à un moment donné, vous avez trop de threads pour que le système puisse fonctionner dans un délai raisonnable.

Chaque fois que vous appelez dispatch_async () et dans ce bloc que vous tentez d’accéder à une E / S (par exemple, si vous lisez certains fichiers ici), il est probable que le thread dans lequel ce bloc est exécuté bloque (se faire interrompre par le système d’exploitation) pendant qu’il attend que les données soient lues depuis le système de fichiers. Le fonctionnement de GCD est tel que lorsqu’il voit qu’un des threads de travail est bloqué sur les E / S et que vous lui demandez toujours de faire plus de travail en même temps, il ne fera que générer un nouveau thread de travail. Ainsi, si vous essayez d’ouvrir 50 fichiers sur une queue simultanée, il est probable que vous finirez par provoquer l’apparition de ~ 50 threads par GCD.

Il y a trop de threads pour que le système fonctionne correctement, et vous finissez par affamer votre thread principal pour le processeur.

La solution consiste à utiliser une queue série au lieu d’une queue simultanée pour effectuer vos opérations sur fichiers. C’est facile à faire. Vous voudrez créer une queue série et la stocker en tant qu’ivar dans votre object afin de ne pas créer plusieurs files d’attente en 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;

Je crois que Ryan est sur le bon chemin: il y a tout simplement trop de sujets à traiter lorsqu’un projet contient 1 500 fichiers (la quantité que j’ai décidé de tester avec.)

Donc, j’ai refait le code ci-dessus pour fonctionner 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 (NSSsortingng *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 seul thread par fichier. En outre, il semble que l’appel à dispatch_async () sur la main_queue soit la bonne approche: le thread de travail envoie ce bloc au thread principal et n’attend pas qu’il revienne avant de continuer à parsingr le fichier suivant.

Cette implémentation met essentiellement en place une queue “série”, comme Ryan l’a suggéré (la boucle for est la partie série), mais avec un avantage: lorsque la boucle for se termine, nous avons fini de traiter tous les fichiers. dispatch_async (main_queue) bloque là pour faire ce que nous voulons. C’est un très bon moyen de savoir quand la tâche de traitement simultanée est terminée et ce qui n’existait pas dans mon ancienne version.

L’inconvénient est que c’est un peu plus compliqué de travailler avec des données de base sur plusieurs threads. Mais cette approche semble être à l’épreuve des balles pour les projets de 5 000 fichiers (ce qui est le plus élevé que j’ai testé).

Je pense que c’est plus facile à comprendre avec un diagramme:

Pour la situation décrite par l’auteur:

| taskQ | *********** début |

| dispatch_1 *********** | ———

| dispatch_2 ************* | ———

.

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

| queue principale (sync) | ** commencer à envoyer à main |

************************* | –dispatch_1– | –dispatch_2– | –dispatch3– | ****** *********************** | –dispatch_n |,

ce qui rend la queue principale de synchronisation si occupée que finalement la tâche échoue.