ARC, кажется, перевыпускает объекты, на которые ссылаются в блоках, которые создаются и отправляются в цикле.

Я пытаюсь выполнить некоторые сложные вычисления в фоновом потоке с помощью dispatch_async, но объекты, которые я использую в блоках, кажутся перегруженными. Я использую ARC, поэтому я предположил, что мне не нужно особо заботиться о сохранении и выпуске, но либо я пропустил что-то важное, либо ARC перевыпускает объекты в моем случае.

Проблема появляется только если

  • Я вызываю dispatch_async, создавая блок в цикле for
  • Я ссылаюсь на объект в блоке, созданном вне блока
  • цикл выполняет как минимум две итерации (таким образом создаются и добавляются в очередь как минимум два блока)
  • используется конфигурация сборки RELEASE (поэтому, вероятно, это связано с некоторой оптимизацией)

Кажется, это не имеет значения

  • будь то последовательная или параллельная очередь
  • какой предмет используется

Этот вопрос касается не выпуска блоков в конфигурации RELEASE (как в iOS 5 блоков аварийно завершает работу только при выпуске сборки), но объекты, на которые есть ссылки в блоке, перевыпущены.

Я создал небольшой пример, используя объект NSURL:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    NSURL *theURL = [NSURL URLWithString:@"/Users/"];
    dispatch_queue_t myQueue = dispatch_queue_create("several.blocks.queue", DISPATCH_QUEUE_SERIAL);

    dispatch_async(myQueue, ^(){
        NSURL *newURL = [theURL URLByAppendingPathComponent:@"test"];
        NSLog(@"Successfully created new url: %@ in initial block", newURL);
    });

    for (int i = 0; i < 2; i++)
    {
        dispatch_async(myQueue, ^(){
            NSURL *newURL = [theURL URLByAppendingPathComponent:@"test"];
            NSLog(@"Successfully created new url: %@ in loop block %d", newURL, i);
        });
    }
}

Первый блок, который не находится в цикле for, будет работать без проблем. Как и второй, если цикл имеет только одну итерацию. Однако в данном примере он выполняет две итерации и аварийно завершает работу, если выполняется с конфигурацией RELEASE. Включение NSZombie в схеме выводит это:

2013-01-07 23:33:33.331 BlocksAndARC[17185:1803] Successfully created new url: /Users/test in initial block
2013-01-07 23:33:33.333 BlocksAndARC[17185:1803] Successfully created new url: /Users/test in loop block 0
2013-01-07 23:33:33.333 BlocksAndARC[17185:1803] *** -[CFURL URLByAppendingPathComponent:]: message sent to deallocated instance 0x101c32790

с остановкой отладчика на вызове URLByAppendingPathComponent в блоке цикла for.

При использовании параллельной очереди неудачный вызов на самом деле будет вызовом release с _Block_release в стеке вызовов:

2013-01-07 23:36:13.291 BlocksAndARC[17230:5f03] *** -[CFURL release]: message sent to deallocated instance 0x10190dd30
(lldb) bt
* thread #6: tid = 0x3503, 0x00007fff885914ce CoreFoundation`___forwarding___ + 158, stop reason = EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0)
    frame #0: 0x00007fff885914ce CoreFoundation`___forwarding___ + 158
    frame #1: 0x00007fff885913b8 CoreFoundation`_CF_forwarding_prep_0 + 232
    frame #2: 0x00007fff808166a3 libsystem_blocks.dylib`_Block_release + 202
    frame #3: 0x00007fff89f330b6 libdispatch.dylib`_dispatch_client_callout + 8
    frame #4: 0x00007fff89f38317 libdispatch.dylib`_dispatch_async_f_redirect_invoke + 117
    frame #5: 0x00007fff89f330b6 libdispatch.dylib`_dispatch_client_callout + 8
    frame #6: 0x00007fff89f341fa libdispatch.dylib`_dispatch_worker_thread2 + 304
    frame #7: 0x00007fff852f0cab libsystem_c.dylib`_pthread_wqthread + 404
    frame #8: 0x00007fff852db171 libsystem_c.dylib`start_wqthread + 13

но это, вероятно, просто из-за немного другого времени.

Я думаю, что обе ошибки указывают на то, что объект NSURL, на который ссылается theURL, перегружен. Но почему? Я что-то пропустил или это баг в сочетании АРК и блоков?

Я ожидаю, что либо перед вызовом dispatch_async, либо при реализации dispatch_async (в любом случае: внутри цикла for, один раз для каждого dispatch_async-вызова) каждая переменная, на которую есть ссылка внутри блока, сохраняется и освобождается в конце (но внутри) блока.

Что на самом деле происходит, так это то, что переменные retained один раз для появления dispatch_async в коде, но release вызывается в конце блока, поэтому всякий раз, когда он выполняется, что приводит к большему количеству release вызовов, чем retain вызовов в цикле.

Но, возможно, я что-то упускаю из виду. Есть ли лучшее объяснение? Я неправильно использовал блоки или ARC, или это ошибка?

EDIT: я попробовал предложение @Joshua Weinberg скопировать ссылочную переменную в локальную внутри цикла for. Это работает в данном примере кода, но не работает, когда задействован вызов функции:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    NSObject *theObject = [[NSObject alloc] init];

    [self blocksInForLoopWithObject:theObject];
}

-(void)blocksInForLoopWithObject:(NSObject *)theObject
{
    dispatch_queue_t myQueue = dispatch_queue_create("several.blocks.queue", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i < 2; i++)
    {
        NSObject *theSameObject = theObject;
        dispatch_async(myQueue, ^(){
            NSString *description = [theSameObject description];
            NSLog(@"Successfully referenced object %@ in loop block %d", description, i);
        });
    }
}

Так почему же это работает в одном случае, но не в другом? Я не вижу разницы.


person Joachim Kurz    schedule 07.01.2013    source источник
comment
Я ожидаю, что либо перед вызовом dispatch_async, либо при реализации dispatch_async (в любом случае: внутри цикла for, один раз для каждого вызова dispatch_async) каждая переменная, на которую ссылаются внутри блока, сохраняется и освобождается в конце (но внутри) блока. В частности, предполагается, что реализация dispatch_async копирует блок, и когда блок перемещается в кучу, он сохраняет переменные, на которые ссылаются. Затем, когда блок освобождается (т. е. когда отправка выполняется), он освобождает свои ссылочные переменные.   -  person newacct    schedule 08.01.2013
comment
Я также заметил, что у вас происходит утечка очереди myQueue (она не освобождается), хотя утечка никогда не вызовет проблемы, которую вы видите.   -  person newacct    schedule 08.01.2013
comment
У меня была эта проблема раньше, но кажется, что она больше не возникает в последней версии XCode (4.6) с последним обновлением инструментов командной строки. Моя версия clang выглядит как Apple LLVM версии 4.2 (clang-425.0.24) (на основе LLVM 3.2svn)   -  person user102008    schedule 28.02.2013


Ответы (2)


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

Что касается того, что вы можете сделать, чтобы обойти это.

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    NSURL *theURL = [NSURL URLWithString:@"/Users/"];
    dispatch_queue_t myQueue = dispatch_queue_create("several.blocks.queue", DISPATCH_QUEUE_SERIAL);

    dispatch_async(myQueue, ^(){
        NSURL *newURL = [theURL URLByAppendingPathComponent:@"test"];
        NSLog(@"Successfully created new url: %@ in initial block", newURL);
    });

    for (int i = 0; i < 2; i++)
    {
        NSURL *localURL = theURL;
        dispatch_async(myQueue, ^(){
            NSURL *newURL = [localURL URLByAppendingPathComponent:@"test"];
            NSLog(@"Successfully created new url: %@ in loop block %d", newURL, i);
        });
    }
}

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

person Joshua Weinberg    schedule 07.01.2013
comment
Интересно, что даже нет необходимости ссылаться на новую локальную переменную localURL внутри блока. Простое добавление присваивания в цикл for и по-прежнему ссылка на старый theURL в блоке предотвращает перевыпуск. Я понятия не имею, почему это. - person Joachim Kurz; 08.01.2013
comment
Я попытался реализовать это изменение в исходном коде, но это не решает проблему. Поэтому я создал еще один пример, который ближе к моему проблемному коду, который дает сбой даже при добавлении локальных переменных (см. вопрос). - person Joachim Kurz; 08.01.2013

Чтобы помочь людям, пытающимся устранить эту проблему, я смог воспроизвести проблему с этой упрощенной версией в моей конфигурации XCode 4.5 Release:

- (id)test {
  return [[NSObject alloc] init];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  id foo = [self test];
  for (int i = 0; i < 2; i++)
  {
    [^(){
      NSLog(@"%@", foo);
    } copy];
  }
  NSLog(@"%@", foo);

  return YES;
}

Судя по профилю, ARC неправильно вставляет релиз в конце внутри цикла.

person user102008    schedule 08.01.2013
comment
Почему метод test необходим для его сбоя? Использование id foo = [[NSObject alloc] init] работает (например: не падает). - person Joachim Kurz; 08.01.2013
comment
@JoachimKurz: ну, test возвращает ссылку на невладение, тогда как alloc/init возвращает ссылку на владение, и компилятор, вероятно, по-разному оптимизирует сохранение и выпуск - person user102008; 08.01.2013