it-swarm.com.ru

dispatch_sync против 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"); никогда не появляется и приложение зависает. НО, (и это то, где вещи становятся действительно странными) - если я запускаю код для небольшого проекта ПЕРВЫМ, а затем запускаю его для большого проекта, все работает отлично. Проблема возникает только тогда, когда я запускаю код в большом проекте.

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

dispatch_async(dispatch_get_main_queue(), ^{

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

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

52
Bryan

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

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

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

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

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;

53
Ryan

Я считаю, что Райан находится на правильном пути: слишком много потоков порождается, когда в проекте 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'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
    }

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

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

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

5
Bryan

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

Для ситуации автор описал:

| TaskQ | *********** начать |

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

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

,.

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

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

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

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

0
Damon Yuan