2015-10-01 2 views
5

Обновлено: Я подготовил образец, который воспроизвести проблему без магического record.Please загрузить тестовый проект, используя следующий URL: https://www.dsr-company.com/fm.php?Download=1&FileToDL=DeadLockTest_CoreDataWithoutMR.zipXcode 7.0 IOS9 SDK: взаимоблокировки при выполнении выборки запроса с performBlockAndWait

Предоставленный проект имеет следующая проблема: deadlock on fetch in performBlockAndWait вызывается из основного потока.

Проблема воспроизводится, если код скомпилирован с использованием версии XCode> 6.4. Проблема не воспроизводится, если код скомпилирован с использованием xCode == 6.4.

Старый вопрос:

Я работаю на поддержку IOS мобильных приложений. После недавнего обновления Xcode IDE с версии 6.4 до версии 7.0 (с поддержкой IOS 9) у меня возникла критическая проблема - зависание приложений. Такая же сборка приложения (из тех же источников) с xCode 6.4 работает нормально. Итак, если приложение построено с использованием xCode> 6.4 - приложение зависает в некоторых случаях. , если приложение построено с использованием xCode 6.4 - приложение работает нормально.

Я потратил некоторое время на исследование проблемы, и в результате я подготовил тестовое приложение с похожим случаем, как в моем приложении, которое воспроизводит проблему. зависание тестового приложения на Xcode> = 7.0, но корректно работает на Xcode 6,4

Скачать ссылку источников тестов: https://www.sendspace.com/file/r07cln

Требования, предъявляемые к тест-приложения: 1. какао менеджер стручки должны быть установленный в системе 2. Магический реестр версии 2.2.

Приложение для тестирования работает следующим образом: 1. В начале приложения создается тестовая база данных с 10000 записями простых объектов и сохраняется в постоянном хранилище. 2. На первом экране приложения в методе viewWillAppear: он запускает тест, который вызывает тупик. используется алгоритм После:

-(NSArray *) entityWithId: (int) entityId inContext:(NSManagedObjectContext *)localContext 
{ 
    NSArray * results = [TestEntity MR_findByAttribute:@"id" withValue:[ NSNumber numberWithInt: entityId ] inContext:localContext]; 
    return results; 
} 

….. 
int entityId = 88; 
NSManagedObjectContext *childContext1 = [NSManagedObjectContext MR_context]; 
childContext1.name = @"childContext1"; 

NSManagedObjectContext *childContext2 = [NSManagedObjectContext MR_context]; 
childContext2.name = @"childContext2"; 

NSArray *results = [self entityWithId:entityId inContext: childContext2]; 

for(TestEntity *d in results) 
{ 
    NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); /// this line is the reason of the hangup 
} 

dispatch_async(dispatch_get_main_queue(),^
       { 
        int entityId2 = 11; 
        NSPredicate *predicate2 = [NSPredicate predicateWithFormat:@"id=%d", entityId2]; 
        NSArray *a = [ TestEntity MR_findAllWithPredicate: predicate2 inContext: childContext2]; 
        for(TestEntity *d in a) 
        { 
         NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); 
        } 
       }); 

Два управляемых контексты объектов создаются с типом параллелизмом == NSPrivateQueueConcurrencyType (пожалуйста, проверьте код MR_context магической звукозаписывающей рамок). Оба контекста имеют родительский контекст с тип параллелизма = NSMainQueueConcurrencyType. Из основного потока приложение выполняет выборку синхронизацией (MR_findByAttribute и MR_findAllWithPredicate используются executeBlockAndWait с запросом выборки внутри). После первой выборки вторая выборка - это расписание по основному потоку с помощью dispatch_async().

В результате приложение зависает. Кажется, что зашел тупик, пожалуйста, проверьте скриншот стека:

 вот ссылка, моя репутация слишком низкая, чтобы отправлять изображения. https://cdn.img42.com/34a8869bd8a5587222f9903e50b762f9.png)

Если комментировать строку
NSLog (@ "е от fetchRequest% @ с именем = '% @'", d, d.name); /// эта строка является причиной зависания

(это строка 39 в ViewController.м тестового проекта) приложение будет работать нормально. Я считаю, что это связано с тем, что не читается поле имени тестового объекта.

Итак, с прокомментированной строкой NSLog (@ "e из fetchRequest% @ с именем = '% @'", d, d.name);
не происходит зависания на двоичных файлах, построенных как с Xcode 6.4, так и с Xcode 7.0.

С недоказанной строкой NSLog (@ "e из fetchRequest% @ с именем = '% @'", d, d.name);

есть бифокальный бит, построенный с помощью Xcode 7.0, и нет бинания на двоичном коде, построенном с Xcode 6.4.

Я считаю, что проблема возникает из-за ленивой загрузки данных сущности.

Есть ли проблемы с описанным случаем? Буду благодарен за любую помощь.

+0

Я считаю, что в недавних SDK для iOS 9 есть проблема или несовместимость, но чтобы помочь людям понять это, сделаем ваш пост более ясным. Прежде всего, можете ли вы загрузить свой пример в github? (Chrome не разрешил мне скачать). Второе: удалите все изменения в сделанных вами контейнерах. Удалите неиспользуемый код, сделайте пример чистым. Краткая история проблемы: диспетчерский блок с дочерним контекстом будет зависеть от него, если 1) отправлен из -viewWillAppear: и 2) перед отправкой использовался управляемый объект (с ошибкой) (как и в NSLog: d. name) Та же проблема с прямым CoreDa – MikeR

ответ

6

Вот почему я не использую фреймворки, которые абстрактно (т. Е. Скрывают) слишком много деталей основных данных. Он имеет очень сложные шаблоны использования, и иногда вам нужно знать детали того, как они взаимодействуют.

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

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

Я не собираюсь вас шокировать, хотя это может показаться на первый взгляд красным. Я хочу помочь обучить вас (и я использовал это как возможность взглянуть на MR).

С очень быстрым взглядом на МР, я бы сказал, что у вас есть некоторые недоразумения в том, что делает MR, а также общие правила параллелизма основных данных.

Во-первых, вы говорите, это ...

Два управляемых контексты объектов создаются с помощью типа параллелизм == NSPrivateQueueConcurrencyType (пожалуйста, проверьте код MR_context из волшебной звукозаписывающей рамок). Оба контекста имеют родительский контекст с тип параллелизма = NSMainQueueConcurrencyType.

который не представляется правдой. Два новых контекста, действительно, являются контекстами частной очереди, но их родительский (в соответствии с кодом, который я смотрел на github) является магическим MR_rootSavingContext, который сам также является контекстом частной очереди.

Давайте разберем ваш пример кода.

NSManagedObjectContext *childContext1 = [NSManagedObjectContext MR_context]; 
childContext1.name = @"childContext1"; 

NSManagedObjectContext *childContext2 = [NSManagedObjectContext MR_context]; 
childContext2.name = @"childContext2"; 

Итак, теперь у вас есть два МОС частных очередей (childContext1 и childContext2), как дети другого анонимного MOC частного очереди (мы будем называть savingContext).

NSArray *results = [self entityWithId:entityId inContext: childContext2]; 

Затем вы выполняете выборку на childContext1. Этот код на самом деле ...

-(NSArray *) entityWithId:(int)entityId 
       inContext:(NSManagedObjectContext *)localContext 
{ 
    NSArray * results = [TestEntity MR_findByAttribute:@"id" 
              withValue:[NSNumber numberWithInt:entityId] 
              inContext:localContext]; 
    return results; 
} 

Теперь мы знаем, что localContext в этом методе, в этом случае, другой указатель на childContext2 который является MOC частной очереди. Это на 100% соответствует правилам параллелизма для доступа к MOC с частной очередью за пределами вызова performBlock. Однако, поскольку вы используете другой API, а имя метода не помогает узнать, как осуществляется доступ к MOC, нам нужно взглянуть на этот API и посмотреть, скрывает ли он performBlock, чтобы узнать, правильно ли вы обращаетесь к нему.

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

Теперь мы добираемся куда-то. Эта функция пытается безопасно получить доступ к MOC, но использует performBlockAndWait, который будет блокироваться при его вызове.

Это чрезвычайно важная информация, потому что вызов этого из-за неправильного места может действительно вызвать тупик. Таким образом, вы должны быть в курсе, что performBlockAndWait вызывается в любое время, когда вы выполняете запрос на выборку. Мое личное правило заключается в никогда использовать performBlockAndWait, если нет абсолютно никакой другой опции.

Однако этот вызов здесь должен быть полностью безопасным ... если предположить, что он не вызывается из контекста родительского MOC.

Итак, давайте рассмотрим следующий фрагмент кода.

for(TestEntity *d in results) 
{ 
    NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); /// this line is the reason of the hangup 
} 

Теперь это не ошибка MagicalRecord, потому что MR даже не используется непосредственно здесь. Тем не менее, вас обучили использовать эти методы MR_, которые не требуют знания модели параллелизма, поэтому вы либо забываете, либо никогда не изучаете правила параллелизма.

Объекты в массиве results являются управляемыми объектами, которые живут в контексте частной очереди childContext2. Таким образом, вы можете никогда не обращаться к ним, не отдавая должное правилам параллелизма. Это явное нарушение правил параллелизма. При разработке приложения вы должны включить параллельную отладку с аргументом -com.apple.CoreData.ConcurrencyDebug 1.

Этот фрагмент кода должен быть обернут либо performBlock, либо performBlockAndWait. Я почти никогда не использую performBlockAndWait для чего-либо, потому что у него так много недостатков - одним из них является тупик. Фактически, только вид использования performBlockAndWait является очень сильным признаком того, что ваш тупик происходит там, а не в строке кода, который вы указываете. Однако, в этом случае, по крайней мере, в безопасности, как предыдущая выборка, так давайте сделаем это немного безопаснее ...

[childContext2 performBlockAndWait:^{ 
    for (TestEntity *d in results) { 
     NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); 
    } 
}]; 

Далее, вы посылаете в основной поток. Это потому, что вы просто хотите, чтобы что-то произошло в следующем цикле цикла событий, или это потому, что этот код уже запущен на каком-то другом потоке? Кто знает. Тем не менее, у вас есть те же проблемы здесь (я переформатировал ваш код для удобства чтения в качестве сообщения).

dispatch_async(dispatch_get_main_queue(), ^{ 
    int entityId2 = 11; 
    NSPredicate *predicate2 = [NSPredicate predicateWithFormat:@"id=%d", entityId2]; 
    NSArray *a = [TestEntity MR_findAllWithPredicate:predicate2 
              inContext:childContext2]; 
    for (TestEntity *d in a) { 
     NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); 
    } 
}); 

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

Основываясь на этом, только реальные проблемы, я вижу это ...

  1. MR кажется чтить правила основных данных параллелизма в их API, но вы все равно должны следовать правилам основных данных параллелизма при доступе ваши управляемые объекты.

  2. Мне действительно не нравится использование performBlockAndWait, так как это просто проблема, ожидающая своего появления.

Теперь давайте посмотрим на скриншот вашей завивки. Хм ... это классический тупик, но это бессмысленно, потому что тупик происходит между основной нитью и потоком MOC. Это может произойти только в том случае, если MOC главной очереди является родителем этого MOC частной очереди, но код показывает, что это не так.

Хммм ... это не имеет смысла, поэтому я загрузил ваш проект и посмотрел исходный код в загруженном вами блоке. Теперь эта версия кода использует MR_defaultContext как родительский элемент всех MOC, созданных с помощью MR_context. Итак, MOC по умолчанию, действительно, MOC главной очереди, и теперь все это имеет смысл.

У вас есть MOC как ребенок главной очереди MOC. Когда вы отправляете этот блок в основную очередь, он теперь работает как блок в главной очереди. Затем код вызывает performBlockAndWait в контексте, который является дочерним элементом MOC для этой очереди, который является огромным нет-нет, и вы почти гарантированно получите тупик.

Итак, похоже, что с тех пор MR сменил свой код на использование главной очереди в качестве родителя новых контекстов для использования частной очереди в качестве родителя новых контекстов (скорее всего, из-за этой точной проблемы). Итак, если вы обновляетесь до последней версии МР, вы должны быть в порядке.

Однако я все же предупреждаю вас, что если вы хотите использовать MR многопоточными способами, вы должны точно знать, как они обрабатывают правила параллелизма, и вы также должны быть уверены, что выполняете их в любое время, когда получаете доступ к каким-либо основным данным объекты, которые не проходят через MR API.

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

Наконец, вы почти никогда не должны использовать performBlockAndWait, если не знаете точно, почему его единственный вариант. Если он будет использоваться как часть API под вами, вам еще страшнее ... по крайней мере, мне.

Я надеюсь, что эта небольшая прогулка просветила и помогла вам (и, возможно, некоторым другим). Это, безусловно, пролило немного света для меня и помогло восстановить некоторые из моих предыдущих необоснованных пугливости.

Редактировать

Это в ответ на пример «не-волшебно-рекорд» вы предоставили.

Проблема с этим кодом - это то же самое, что я описал выше, относительно того, что происходило с MR.

У вас есть контекст частной очереди, как дочерний элемент в контексте главной очереди.

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

Это называется тупиком, но более описательный (и соблазнительный) термин является смертельным объятием.

Исходный код работает на основном потоке. Он вызывает детский контекст, чтобы что-то делать, и он ничего не делает до тех пор, пока этот ребенок не завершится.

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

Ни один из них не может двигаться вперед.

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

Вы должны NEVER позвонить performBlockAndWait по детскому контексту.

Тот факт, что вы ушли с ним в прошлом, - это просто «случайность», потому что он не должен работать таким образом вообще.

В действительности, вы вряд ли должны каждый раз звонить performBlockAndWait.

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

Во-первых, переписать выборки, так что работает асинхронно ...

- (void)executeFetchRequest:(NSFetchRequest *)request 
        inContext:(NSManagedObjectContext *)context 
       completion:(void(^)(NSArray *results, NSError *error))completion 
{ 
    [context performBlock:^{ 
     NSError *error = nil; 
     NSArray *results = [context executeFetchRequest:request error:&error]; 
     if (completion) { 
      completion(results, error); 
     } 
    }]; 
} 

Затем вы меняете вам код, который вызывает выборки, чтобы сделать что-то вроде этого ...

NSFetchRequest *request = [[NSFetchRequest alloc] init]; 
[request setEntity: testEntityDescription ]; 
[request setPredicate: predicate2 ]; 
[self executeFetchRequest:request 
       inContext:childContext2 
       completion:^(NSArray *results, NSError *error) { 
    if (results) { 
     for (TestEntity *d in results) { 
      NSLog(@"++++++++++ e from fetchRequest %@ with name = '%@'", d, d.name); 
     } 
    } else { 
     NSLog(@"Handle this error: %@", error); 
    } 
}]; 
+0

Вот URL-адрес образца, который воспроизводит проблему без магической записи. https://www.dsr-company.com/fm.php?Download=1&FileToDL=DeadLockTest_CoreData.zip Предоставленный проект имеет следующую проблему: deadlock on fetch in performBlockAndWait вызывается из основного потока. Проблема воспроизводится, если код скомпилирован с использованием версии XCode> 6.4. Проблема не воспроизводится, если код скомпилирован с использованием xCode == 6.4. Есть ли у кого-нибудь идеи? –

+0

@KirillNeznamov - Этот код делает то же самое, что я описал, что используется версия MR, которую вы использовали. См. Редактирование для более подробной информации. –

+0

Спасибо Джоди, я попытаюсь найти заметки в официальной документации по яблоку относительно ответа. Большое спасибо! –

1

Мы перешли до XCode7, и я просто столкнулся с аналогичной проблемой тупика с performBlockAndWait в коде, который отлично работает в XCode6.

Проблема, по-видимому, связана с обращением dispatch_async(mainQueue, ^{ ..., чтобы передать результат из сетевой операции. Этот вызов больше не нужен после того, как мы добавили поддержку параллелизма для CoreData, но почему-то это было оставлено и никогда не создавало проблемы до сих пор.

Возможно, что Apple что-то изменило за кулисами, чтобы сделать потенциальные тупики более явными.

+1

Я сообщил о проблеме поддержки Apple, они сказали, что это ошибка. Я отправил им измененные источники, которые воспроизводят проблему без магической структуры записей. Поэтому нам стоит подождать, пока Apple его исправит. Кстати, они сказали, что проблема может быть решена с помощью метода reset() NSManagedContext –

Смежные вопросы