Подтвердить что ты не робот

Dispatch_sync vs. dispatch_async в главной очереди

Потерпите меня, это займет некоторое объяснение. У меня есть функция, которая выглядит так, как показано ниже.

Контекст: "aProject" - это объект Core Data с именем LPProject с массивом с именем "memberFiles", который содержит экземпляры другого объекта Core Data, называемого LPFile. Каждый LPFile представляет файл на диске, и мы хотим открыть каждый из этих файлов и проанализировать его текст, ища инструкции @import, указывающие на ДРУГИЕ файлы. Если мы находим операторы @import, мы хотим найти файл, на который они указывают, а затем "связать" этот файл с этим, добавив отношение к основному объекту данных, который представляет первый файл. Поскольку все это может занять некоторое время на больших файлах, мы сделаем это с основного потока, используя 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
        }
    }
}

Теперь, где вещи становятся странными:

Этот код работает, но я вижу странную проблему. Если я запустил его на LPProject с несколькими файлами (около 20), он работает отлично. Однако, если я запустил его на LPProject, у которого больше файлов (скажем, 60-70), он корректно выполняет NOT. Мы никогда не возвращаемся к основному потоку, NSLog(@"got to main thread"); никогда не появляется и приложение зависает. НО, (и это то, где вещи становятся ДЕЙСТВИТЕЛЬНО странными) --- если я запускаю код в маленьком проекте FIRST и THEN запускаю его на большом проекте, все работает отлично. Это ТОЛЬКО, когда я сначала запускаю код в большом проекте, что проблема возникает.

И вот кикер, если я изменю вторую строку отправки на это:

dispatch_async(dispatch_get_main_queue(), ^{

(То есть, используйте async вместо sync для отправки блока в основную очередь), все работает все время. В совершенстве. Независимо от количества файлов в проекте!

Я затрудняюсь объяснить это поведение. Любая помощь или советы о том, что тестировать дальше, будут оценены.

4b9b3361

Ответ 1

Это распространенная проблема, связанная с дисковым вводом-выводом и GCD. В принципе, GCD, вероятно, порождает один поток для каждого файла, и в определенный момент у вас слишком много потоков для обслуживания системы за разумное время.

Каждый раз, когда вы вызываете dispatch_async(), и в этом блоке вы пытаетесь использовать любой ввод-вывод (например, похоже, что вы читаете некоторые файлы здесь), вероятно, поток, в котором этот блок кода выполнение блокируется (приостанавливается ОС), пока он ожидает, что данные будут считаны из файловой системы. Способ работы GCD таков, что когда он видит, что один из его рабочих потоков заблокирован на вводе-выводе, и вы все еще просите его делать больше работы одновременно, он просто порождает новый рабочий поток. Таким образом, если вы попытаетесь открыть 50 файлов в параллельной очереди, вероятно, вы закончите тем, что GCD будет генерировать ~ 50 потоков.

Это слишком много потоков для полноценного обслуживания системы, и вы в конечном итоге голодали на главный поток для CPU.

Способ исправить это - использовать последовательную очередь вместо параллельной очереди для выполнения ваших файловых операций. Это легко сделать. Вы захотите создать очередную очередь и сохранить ее как ivar в своем объекте, чтобы не создавать несколько последовательных очередей. Поэтому удалите этот вызов:

dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

Добавьте это в свой метод init:

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

Добавьте это в свой метод dealloc:

dispatch_release(taskQ);

И добавьте это как ivar в объявление класса:

dispatch_queue_t taskQ;

Ответ 2

Я считаю, что Райан находится на правильном пути: существует слишком много потоков, возникающих, когда проект имеет 1500 файлов (количество, которое я решил проверить с помощью.)

Итак, я переработал приведенный выше код, чтобы работать следующим образом:

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

Итак, в основном, мы теперь создаем один поток, который читает все файлы вместо одного потока на файл. Кроме того, оказывается, что вызов dispatch_async() на main_queue является правильным подходом: рабочий поток отправит этот блок в основной поток и НЕ ждет его возвращения, прежде чем приступать к сканированию следующего файла.

Эта реализация по существу устанавливает "последовательную" очередь, как предложил Райан (цикл for является его последовательной частью), но с одним преимуществом: когда цикл for заканчивается, мы закончили обработку всех файлов, и мы можем просто вставьте блок dispatch_async (main_queue), чтобы делать все, что мы хотим. Это очень хороший способ сказать, когда задача параллельной обработки завершена и которая не существует в моей старой версии.

Недостатком здесь является то, что он немного сложнее работать с Core Data для нескольких потоков. Но этот подход кажется пуленепробиваемым для проектов с 5000 файлами (это самый высокий уровень, который я тестировал).

Ответ 3

Я думаю, что с диаграммой легче понять:

В этой ситуации автор описал:

| taskQ | *********** начать |

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

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

.

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

| главная очередь (синхронизация) | ** начало отправки на главную |

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

которые делают основную очередь синхронизации настолько занятой, что, наконец, не удается выполнить задачу.