Метод отправки Objective-C с блоком, который будет выполняться в потоке *вызывающего*

Я пишу класс черного ящика, который выполняет тяжелую обработку в фоновом режиме с помощью Grand Central Dispatch. Я намерен предоставить API стиля продолжения, что-то вроде:

- (void) processHeavyStuff:(id) someParam thenDo:(ContinuationBlock)followup;

Который клиент может вызвать, например, так:

[myBlackBox processHeavyStuff:heavyOne thenDo: ^(Dalek* result){
    [self updateDisplayWithNewDalek:result];
}];

Что обычно делается, так это то, что реализация processHeavyStuff:thenDo: вызывает свой блок продолжения в основном потоке, используя dispatch_get_main_queue(). См. метод вызова модели с блоком, который будет запустить в основном потоке для примера.

Однако этот распространенный сценарий предполагает, что клиент звонит из основного потока. Я хотел бы быть более общим и вызвать блок продолжения в потоке вызывающей стороны, который может быть или не быть основным потоком. Это позволит, например, хорошо работать с многопоточными клиентами Core Data, где NSManagedObjectContext является локальным для потока. Есть ли хороший шаблон для этого?

Используя –[NSObject performSelector:onThread:withObject:waitUntilDone:], я вижу, что могу определить вспомогательный метод:

- (void) callContinuation:(ContinuationBlockWithNoArgument) followup
{
    followup();
}

А затем выполните этот селектор в потоке вызывающего абонента:

- (void) processHeavyStuff:(id) someParam thenDo:(ContinuationBlock)followup
{
    NSSthread *callerThread = [NSThread currentThread];

    dispatch_async(self.backgroundQueue, ^ {
        Dalek *newDalek = [self actuallyDoTheHeavyProcessing:someParam];

        [self performSelector:@selector(callContinuation:) onThread:callerThread
            withObject: ^{
                followup(newDalek);
            }
            waitUntilDone:NO];
    });
}

Я думаю, это может сработать, и я собираюсь попробовать. Но есть ли что-то менее надуманное? Возможно версия performSelector:onThread: для блоков?

PS: Для ясности я исключил все вызовы управления памятью из приведенных выше фрагментов. Например, блок followup основан на стеке и должен быть скопирован в кучу, чтобы использовать его в другом потоке...

Изменить: я узнал, что Майк Эш использует очень похожий подход с:

void RunOnThread(NSThread *thread, BOOL wait, BasicBlock block)
{
    [[[block copy] autorelease] performSelector: @selector(my_callBlock) onThread: thread withObject: nil waitUntilDone: wait];
}

Где my_callBlock определяется в категории NSObject:

@implementation NSObject (BlocksAdditions)
- (void)my_callBlock
{
    void (^block)(void) = (id)self;
    block();
}
@end;

person Jean-Denis Muys    schedule 09.09.2011    source источник


Ответы (4)


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

- (void)processHeavyStuff:(id)someParam thenDo:(ContinuationBlock)followup {
    dispatch_queue_t callerQueue = dispatch_get_current_queue();
    dispatch_retain(callerQueue);

    dispatch_async(self.backgroundQueue, ^ {
        Dalek *newDalek = [self actuallyDoTheHeavyProcessing:someParam];

        dispatch_async(callerQueue, ^{
            followUp(dalek);
            dispatch_release(callerQueue);
        });
    });
}

Одна вещь, в которой я не совсем уверен, заключается в том, нужно ли вам сохранять callerQueue и затем освобождать ее. Я думаю, что нет.

Надеюсь, это поможет вам!

EDIT: добавлено сохранение/освобождение

person nubbel    schedule 09.09.2011
comment
Я подозреваю, что dispatch_get_current_queue() не работает, когда клиентский код не использует GCD. Apple говорит: эта функция определена так, чтобы никогда не возвращать NULL. При вызове вне контекста отправленного блока эта функция возвращает параллельную очередь по умолчанию. Таким образом, от клиента, вызывающего из основного потока, это вернет не основную очередь, а параллельную очередь по умолчанию, что было бы неправильно. То же самое для многопоточного кода, который не использует GCD. - person Jean-Denis Muys; 09.09.2011
comment
@nubbel: Да, вам нужно сохранить callerQueue сразу после его получения, и вам нужно освободить его в конце блока, который вы отправляете ему асинхронно. - person Marco Masser; 09.09.2011
comment
@Jean-DenisMuys В документах говорится: при вызове из-за пределов контекста отправленного блока эта функция возвращает основную очередь, если вызов выполняется из основного потока. Однако ваш второй пункт верен. - person nubbel; 13.02.2012

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

Очередь, возвращенная dispatch_get_current_queue(), может быть использована вызывающей стороной способами, которые вы не ожидаете и не можете предвидеть; возможно, они вызывают dispatch_suspend() к тому времени, когда вы ставите в очередь обработчик завершения. Вы также не можете предположить, что можете отправить performSelector: в возвращаемое значение [NSThread currentThread] в какой-то произвольный момент в будущем; что, если этот поток завершится к тому времени, когда вы поставите в очередь обработчик завершения? Что, если в этом потоке нет цикла выполнения? В обоих случаях ваш обработчик завершения никогда не запускается.

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

person Nick Hutchinson    schedule 18.12.2011
comment
Я только что увидел твой ответ после ответа Джоди. Я понимаю ваше возражение. Но это применимо и к performSelector:onThread:. Я думаю, что вызов продолжения в вызывающем потоке имеет смысл и соблюдает принцип наименьшего удивления. Если вызывающая сторона хочет сделать дополнительные вещи, как вы описываете, она всегда может вызвать мой API в основном потоке, даже из вспомогательного потока. - person Jean-Denis Muys; 15.04.2012
comment
Наконец, вызов продолжения в том же потоке позволяет избежать переключения контекста (по крайней мере, теоретически). - person Jean-Denis Muys; 15.04.2012
comment
Я согласен, что это плохая идея. У вас нет причин ожидать, что поток все еще существует или, если он существует, что он взаимодействует с механизмами, позволяющими ему выполнять вашу задачу. По сути, его нужно было бы припарковать в цикле выполнения, и если он собирается это сделать, зачем выполнять какую-либо асинхронную работу. Вместо этого просто оставьте вызывающей стороне через переданный блок возможность направить работу продолжения туда, куда нужно. Или позвольте вызывающему абоненту пройти в очередь, в которую должно быть отправлено продолжение. - person Ken Thomases; 15.04.2012

Вы можете просто добавить еще один параметр в свой метод -processHeavyStuff:thenDo:, указав очередь, в которой вы хотите запустить блок. Этот подход гораздо более гибкий и используется в NSNotificationCenter в -addObserverForName:object:queue:usingBlock: (хотя он и использует NSOperationQueues, но принцип тот же). Просто не забывайте, что вы должны сохранять и освобождать очередь, которую вы проходите.

person Marco Masser    schedule 09.09.2011
comment
Действительно, но тогда это накладывает на клиента бремя понимания, что такое очередь, где ее взять, какое значение передать в этом параметре. Он также раскрывает то, что с точки зрения клиента является деталью реализации. Учитывая стоимость задействованных вычислений, невозможно скрыть асинхронность от клиента, поэтому блочный API не хуже (и, вероятно, лучше, если оставить в стороне переносимость), чем любой другой. - person Jean-Denis Muys; 09.09.2011
comment
ИМХО, если для вызывающей стороны важно, где должен выполняться блок, то эта деталь реализации должна быть раскрыта. - person Marco Masser; 10.09.2011
comment
Что ж, многие фрагменты кода в Cocoa зависят от потоков. Например, все, что касается объекта Core Data. На самом деле, если вы не уверены в обратном, самое безопасное предположение состоит в том, что любая часть кода зависит от потока с Cocoa. Когда я получаю блок от своего клиента, если я выполняю его в другом потоке, я могу вызвать незаметные (или не очень) ошибки. Принцип наименьшего удивления осуждает это. Кроме того, если у меня есть способ изолировать моих клиентов от тонкостей потоковой передачи, я не вижу причин, по которым мне не следует этого делать. - person Jean-Denis Muys; 11.09.2011
comment
Люди, звонящие из основного потока, ожидают, что их вызовут в основном потоке. Но люди, звонящие из фонового потока, предположительно знают, что они делают, и предпочли бы указать, хотят ли они обратного вызова в основном потоке или все же в фоновом. В конце концов, большую часть времени -processHeavyStuff:thenDo: все равно придется запускать в пользовательском интерфейсе, чтобы показать результаты... почему бы вам не создать две записи API? Один с параметром и один с dispatch_get_current_queue()? Многие методы performSelector:... имеют такие варианты. - person Grzegorz Adam Hankiewicz; 18.12.2011

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

Поскольку вопрос все еще остается без ответа, может быть, вы все еще ищете ответ? Если нет, не могли бы вы указать, что лучше всего работает для вас?

// Continuation block is executed on an arbitrary thread.
// Caller can change context to specific queue/thread in
// the continuation block if necessary.
- (void) processHeavyStuff:(id) someParam thenDo:(ContinuationBlock)followup;

// Continuation block is executed on the given GCD queue
- (void) processHeavyStuff:(id) someParam thenOnQueue:(dispatch_queue_t)queue do:(ContinuationBlock)followup;

// Continuation block is executed on the given thread
- (void) processHeavyStuff:(id) someParam thenOnThread:(NSThread*)thread do:(ContinuationBlock)followup;

// Continuation block is executed on *this* thread
- (void) processHeavyStuff:(id) someParam thenDoOnThisThread:(ContinuationBlock)followup;

// Continuation block is executed on main thread
- (void) processHeavyStuff:(id) someParam thenDoOnMainThread:(ContinuationBlock)followup;
person Jody Hagins    schedule 13.04.2012
comment
Так вы обогащаете API и уточняете имена. Ваш processHeavyStuff:thenDoOnThisThread: эквивалентен тому, что я предложил. Открывая так много опций, вы значительно раздуваете API, тем более что вызывающая сторона всегда может перенаправить свой блок в любой поток или очередь. Наконец, два наиболее распространенных варианта использования — это поток вызывающей стороны и основной поток. В конце концов я решил следовать тому плану, который дал, тем более, что Майк Эш делает то же самое, что для меня является довольно сильным одобрением. Однако сделать имя метода более явным — хорошая идея. Спасибо. - person Jean-Denis Muys; 15.04.2012