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), он НЕ работает правильно. Мы никогда не возвращаемся к основному потоку, NSLog(@"got to main thread"); никогда не появляется, и приложение зависает. НО (и здесь все становится ДЕЙСТВИТЕЛЬНО странным) --- если я СНАЧАЛА запускаю код в маленьком проекте, а ЗАТЕМ запускаю его в большом проекте, все работает отлично. ТОЛЬКО когда я сначала запускаю код в большом проекте, появляется проблема.

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

dispatch_async(dispatch_get_main_queue(), ^{

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

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


person Bryan    schedule 30.06.2011    source источник
comment
Примечание. Для краткости я отредактировал фрагменты кода сканирования и ввода основных данных. Однако я почти уверен, что они не виноваты, потому что они отлично работают, если я помещаю все в один поток, И они отлично работают в многопоточных ситуациях, описанных выше (разогрев всего, сначала запустив небольшой проект и/ или используя dispatch_async() в основной очереди вместо dispatch_sync()).   -  person Bryan    schedule 30.06.2011
comment
Похоже, вы столкнулись с проблемой взаимоблокировки   -  person Dave DeLong    schedule 30.06.2011
comment
Вы должны запустить образец или инструменты для своего приложения, когда оно находится в этом состоянии, чтобы увидеть, что делают все другие потоки. Если они зашли в тупик, то, что происходит, должно быть гораздо более очевидным.   -  person Jon Hess    schedule 01.07.2011
comment
Где вызывается NSManagedObjectContext -save? У вас есть наблюдатель этого уведомления, который заставляет его реагировать на основной поток, используя PerformSelectorOnMainThread?   -  person ImHuntingWabbits    schedule 01.07.2011
comment
Этот вопрос следует отредактировать, чтобы указать, где происходит ввод-вывод отдельных файлов, а не где происходят запросы CoreData. В нынешнем виде это вводит в заблуждение.   -  person Ryan    schedule 02.07.2011
comment
@wabbits: -save вызывается до того, как я ввожу этот метод. Отправка любых изменений в manageObjectContext основного потока в постоянное хранилище до того, как мы создадим новый контекст в фоновом потоке, означает, что фоновый контекст является точной копией контекста основного потока. Я никогда не меняю управляемые объекты в фоновом контексте, поэтому мне никогда не нужно возвращать какие-либо изменения в контекст основного потока. Я просто возвращаюсь к основному потоку и вношу изменения в ЭТОТ контекст.   -  person Bryan    schedule 03.07.2011


Ответы (3)


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

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

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

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

dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

Добавьте это в свой метод инициализации:

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

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

dispatch_release(taskQ);

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

dispatch_queue_t taskQ;

person Ryan    schedule 01.07.2011
comment
У Майка Эша также есть отличная статья об этой проблеме: mikeash .com/pyblog/friday-qa-2009-09-25-gcd-practicum.html - person jscs; 01.07.2011
comment
@Ryan - Спасибо за вклад. Это случалось и со мной, но если бы проблема заключалась в слишком большом количестве одновременных потоков, мы бы ожидали, что большой проект будет терпеть неудачу КАЖДЫЙ раз. В этом случае это РАБОТАЕТ, пока я сначала запускаю код в меньшем проекте. (Обратите внимание, что два проекта представляют собой совершенно отдельные файлы, поэтому никакие файлы не кэшируются и т. д.) - person Bryan; 01.07.2011
comment
Что делать, если включен автоматический подсчет ссылок? - person byJeevan; 26.09.2014

Я считаю, что Райан на правильном пути: просто слишком много потоков создается, когда в проекте 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 файлов (это самый высокий показатель, который я тестировал).

person Bryan    schedule 01.07.2011
comment
Да, ваш первоначальный вопрос не дал понять, что ваши файлы на самом деле были объектами CoreData. Это совершенно другой тип проблемы. Мой ответ касался фактического ввода-вывода файлов. В этот момент я понимаю, что не могу сказать, что вы делаете, не видя полного листинга исходного кода. Я не уверен, когда вы выполняете файловый ввод-вывод или читаете данные из CoreData. Не стесняйтесь указывать источник того, что вы делаете, если хотите получить больше информации. - person Ryan; 02.07.2011

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

Для ситуации, описанной автором:

|задачаQ| ************старт|

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

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

.

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

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

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

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

person Damon Yuan    schedule 12.02.2015