Вы можете использовать cancel / isCancelled с GCD / dispatch_async? - PullRequest
7 голосов
/ 27 марта 2011

Мне было интересно, можете ли вы использовать cancel / cancelAllOperations / .isCancelled с потоком, который вы запустили с GCD?

В настоящее время я просто использую логическое значение в качестве флага, чтобы отменить фоновый процесс.

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

@interface AstoundingView : UIView
    {
    BOOL    pleaseAbandonYourEfforts;
    blah
    }
@implementation AstoundingView
//
// these are the foreground routines...
// begin, abandon and all-done
//
-(void)userHasClickedToBuildASpaceship
    {
    [YourUIStateMachine buildShip];
    [self procedurallyBuildEnormousSpaceship];
    }
-(void)userHasClickedToAbandonBuildingTheSpaceship
    {
    [YourUIStateMachine inbetween];
    pleaseAbandonYourEfforts = false; // that's it!
    }
-(void)attentionBGIsAllDone
    {
// you get here when the process finishes, whether by completion
// or if we have asked it to cancel itself.
    [self typically setNeedsDisplay, etc];
    [YourUIStateMachine nothinghappening];
    }
//
// these are the background routines...
// the kickoff, the wrapper, and the guts
//
// The wrapper MUST contain a "we've finished" message to home
// The guts can contain messages to home (eg, progress messages)
//
-(void)procedurallyBuildEnormousSpaceship
    {
    // user has clicked button to build new spaceship
    pleaseAbandonYourEfforts = FALSE;
    dispatch_async(
        dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
        ^{ [self actuallyProcedurallyBuildInBackground]; }
        );

    // as an aside, it's worth noting that this does not work if you
    // use the main Q rather than a global Q as shown.
    // Thus, this would not work:
    // dispatch_async(dispatch_get_main_queue(), ^{ ...; });
    }

-(void)actuallyProcedurallyBuildInBackground
    {
    // we are actually in the BG here...

    [self setUpHere];

    // set up any variables, contexts etc you need right here
    // DO NOT open any variables, contexts etc in "buildGuts"

    // when you return back here after buildGuts, CLEAN UP those
    // variables, contexts etc at this level.

    // (using this system, you can nest as deep as you want, and the
    // one CHECKER pseudocall will always take you right out.
    // You can insert CHECKERs anywhere you want.)

    [self buildGuts];

    // Note that any time 'CHECKER' "goes off', you must fall-
    // through to exactly here.  This is the common fall-through point.
    // So we must now tidy-up, to match setUpHere.

    [self wrapUpHere];

    // when you get to here, we have finished (or, the user has cancelled
    // the background operation)

    // Whatever technique you use,
    // MAKE SURE you clean up all your variables/contexts/etc before
    // abandoning the BG process.

    // and then you must do this......

    // we have finished. it's critical to let the foreground know NOW,
    // or else it will sit there for about 4 to 6 seconds (on 4.3/4.2)
    // doing nothing until it realises you are done

    dispatch_sync(
        dispatch_get_main_queue(),
        ^{[self attentionBGIsAllDone];} // would setneedsdisplay, etc
        );

    return;
    }
-(void)buildGuts
    {
    // we are actually in the BG here...

    // Don't open any local variables in here.

    CHECKER
    [self blah blah];
    CHECKER
    [self blah blah];
    CHECKER
    [self blah blah];

    // to get stuff done from time to time on the UI, something like...

    CHECKER
    dispatch_sync(
        dispatch_get_main_queue(),
        ^{[supportStuff pleasePostMidwayImage:
            [UIImage imageWithCGImage:halfOfShip] ];}
        );

    CHECKER
    [self blah blah];
    CHECKER
    [self blah blah];
    CHECKER
    [self blah blah];
    for ( i = 1 to 10^9 )
        {
        CHECKER
        [self blah blah];
        }
    CHECKER
    [self blah blah];
    CHECKER
    [self blah blah];
    CHECKER
    [self blah blah];

    return;
    }

и CHECKER ничего не делает, кроме как проверяет, что флаг верен ...

#define CHECKER if ( pleaseAbandonYourEfforts == YES ) \
{NSLog(@"Amazing Interruption System Working!");return;}

Все это прекрасно работает.

Но ........ можно ли использовать cancel / cancelAllOperations / .isCanlled с этим типом использования GCD?

Что здесь за история?Приветствия.


PS - для любых новичков, использующих этот фоновый шаблон «из шести частей».

Обратите внимание, что, как подчеркивает BJ ниже, всякий раз, когда вы выходите из процесса bg ...

Вы должны очистить все открытые переменные!

В моем идиоме вы должны выделить все переменные, контексты, память и т. Д., В частности, в "setUpHere".И вы должны выпустить их в "wrapUpHere".(Эта идиома продолжает работать, если вы идете все глубже и глубже, находясь в BG.)

Поочередно, делайте именно то, что BJ показывает в своем примере.(Если вы используете метод BJ, будьте осторожны, если вы пойдете глубже.)

Какой бы метод вы ни использовали, вы должны очистить все переменные / контексты / память, которые вы открыли, когда выйдете из процесса BG.Надеюсь, это когда-нибудь кому-нибудь поможет!

Ответы [ 2 ]

18 голосов
/ 27 марта 2011

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

Вы спросили, можете ли вы использовать флаги NSOperation для поддержки отмены. Ответ - нет. GCD не основан на NSOperation. Фактически, в Snow Leopard NSOperation и NSOperationQueue были повторно реализованы для внутреннего использования GCD. Так что зависимость наоборот. NSOperation - это конструкция более высокого уровня, чем GCD. Даже если бы вы использовали NSOperation, ваша реализация отмены была бы в значительной степени такой же; вам все равно придется периодически проверять self.isCancelled, чтобы выяснить, следует ли вам отказаться от строительства космического корабля.

Единственное, что меня беспокоит в связи с вашей реализацией макроса CHECKER, - это то, что он реализует неожиданный return. Таким образом, вы должны быть осторожны с утечками памяти. Если вы настроили свой собственный NSAutoreleasePool в фоновом потоке, вам нужно drain его перед возвратом. Если вы alloc редактировали или retain редактировали какие-либо объекты, вам может понадобиться release их перед возвратом.

Поскольку вся эта очистка должна происходить при каждой проверке, вы можете рассмотреть возможность перехода к единой точке возврата. Один из способов сделать это - заключить каждый вызов вашего метода в блок if (pleaseAbandonYourEfforts == NO) { }. Это позволит вам быстро перейти к концу метода после запроса отмены и сохранить вашу очистку в одном месте. Другим вариантом, хотя некоторым это может не нравиться, было бы сделать вызов макроса goto cleanup; и определить метку cleanup: в конце метода, где вы освобождаете все, что должно быть освобождено. Некоторые люди не любят использовать goto почти религиозным образом, но я обнаружил, что прямой переход к метке очистки, подобной этой, часто является более чистым решением, чем альтернативы. Если вам это не нравится, оборачивайте все в блок if так же хорошо.


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

Я чувствую необходимость уточнить мое предыдущее утверждение о наличии единой точки возврата. С макросом CHECKER, как определено выше, метод -buildGuts может возвращать в любую точку, где используется этот макрос. Если есть какие-либо сохраненные объекты, локальные для этого метода, они должны быть очищены перед возвратом. Например, представьте себе очень разумную модификацию вашего -buildGuts метода:

-(void)buildGuts
{
    // we are actually in the BG here...

    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];

    CHECKER
    [self blah blah];
    CHECKER
    [self blah blah];
    CHECKER
    [self recordSerialNumberUsingFormatter:formatter];

    // ... etc ...

    [formatter release];

    return;
}

Обратите внимание, что в этом случае, если макрос CHECKER заставляет нас вернуться до конца метода, то объект в formatter не будет освобожден и будет пропущен . Хотя вызов [self quickly wrap up in a bow] может выполнять очистку для любых объектов, достижимых через переменную экземпляра или через глобальный указатель, он не может освобождать объекты, которые были доступны только локально в методе buildGuts. Вот почему я предложил реализацию goto cleanup, которая будет выглядеть следующим образом:

#define CHECKER if ( pleaseAbandonYourEfforts == YES ) { goto cleanup; }

-(void)buildGuts
{
    // we are actually in the BG here...

    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];

    CHECKER
    [self blah blah];
    CHECKER
    [self blah blah];
    CHECKER
    [self recordSerialNumberUsingFormatter:formatter];

    // ... etc ...

cleanup: 
    [formatter release];

    return;
}

Согласно этой реализации, formatter всегда будет освобожден, независимо от того, когда произойдет отмена.

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

1 голос
/ 15 ноября 2011

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

@interface MyMutableBool : NSObject {
    BOOL value;
}
@property BOOL value;
@end

@implementation MyMutableBool
@synthesize value;
@end

И использовать экземпляр этого для pleaseAbandonYourEfforts. Прежде чем выполнить dispatch_async (т. Е. В procedurallyBuildEnormousSpaceship выше), я отменяю старый запрос и готовлюсь к новому следующим образом:

// First cancel any old outstanding request.
cancelRequest.value = YES;
// Now create a new flag to signal whether or not to cancel the new request.
MyMutableBool *cancelThisRequest = [[[MyMutableBool alloc] init] autorelease];
self.cancelRequest = cancelThisRequest;

Мой блок, выполняющий асинхронную задачу, должен, конечно, проверить cancelThisRequest.value.

...