Как остановить NSOperationQueue во время dispatch_async

Nov 03 2020

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

Я сделал образец кода, чтобы показать вам свою проблему:

__block BOOL queueDidCancel = NO;
NSArray *array = [NSArray arrayWithObjects:@"1",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"10", nil];

NSOperationQueue *myQueue = [NSOperationQueue new];
myQueue.maxConcurrentOperationCount =1;


for (NSString *string in array) {
    [myQueue addOperationWithBlock:^{
        if (queueDidCancel) {return;}
        NSLog(@"run: %@", string);
        dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            if ([string isEqualToString:@"1"]) {
                queueDidCancel = YES;
                [myQueue cancelAllOperations];
            }
        });
    }];
}

Ожидаемый результат NSLog:

run: 1

Получился результат (от 7 до 9):

run: 1
run: 2
run: 3
run: 4
run: 5
run: 6
run: 7
run: 8

Я гуглил часами, но решения не нашел.

Ответы

Chris Nov 04 2020 at 00:13

Думаю, я нашел решение. Вот обновленный код:

NSArray *array = [NSArray arrayWithObjects:@"1",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"10", nil];

NSOperationQueue *myQueue = [NSOperationQueue new];
myQueue.maxConcurrentOperationCount =1;


for (NSString *string in array) {
    [myQueue addOperationWithBlock:^{
        [myQueue setSuspended:YES];
        NSLog(@"run: %@", string);
        dispatch_async(dispatch_get_main_queue(), ^{
            if (![string isEqualToString:@"1"]) {
                [myQueue setSuspended:NO];
            }
        });
    }];
}
skaak Nov 03 2020 at 23:28

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

Затем, если вы отмените в бите dispatch_async, это произойдет слишком долго после выполнения всех блоков. Это то, что показывает ваш вывод. Как упоминалось в комментарии, если вы добавите NSLog, например

dispatch_async(dispatch_get_main_queue(), ^{
            if ([string isEqualToString:@"1"]) {
                queueDidCancel = YES;
                // Add here
                NSLog(@"Going to cancel now");
                [myQueue cancelAllOperations];
            }

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

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

Вот небольшой пример.

NSObject * lock = NSObject.new;      // Use this to sync access
__block BOOL queueDidCancel = NO;

NSOperationQueue *myQueue = [NSOperationQueue new];
myQueue.maxConcurrentOperationCount =1;

for (NSString *string in array) {
    // Here you also need to add some logic, e.g. as below
    // Note the sync access
    @synchronized ( lock ) {
      if (queueDidCancel) { break; }
    }

    [myQueue addOperationWithBlock:^{

        // You need to sync access to queueDidCancel especially if
        // you access it from main and the queue or if you increase
        // the concurrent count
        // This lock is one way of doing it, there are others
        @synchronized ( lock ) {
          // Here is your cancel logic! This is fine here
          if (queueDidCancel) {return;}
        }

        NSLog(@"run: %@", string);

        dispatch_async(dispatch_get_main_queue(), ^{

            if ([string isEqualToString:@"1"]) {
                // Again you need to sync this
                @synchronized ( lock ) {
                  queueDidCancel = YES;
                }
                // This is not needed your logic should take care of it ...
                // The problem is that running threads will probably
                // keep on running and you need logic to stop them
                // [myQueue cancelAllOperations];
            }
        });
    }];
}

Теперь этот пример делает то же, что и ваш, но с немного большей блокировкой и немного большей логикой, и НЕТ cancelAllOperations или приостановлено = ДА. Это не будет делать то, что вы хотите, поскольку даже при этом запущенные потоки, как правило, завершаются, и вам нужна логика, чтобы остановить его.

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

РЕДАКТИРОВАТЬ

Основываясь на большом количестве комментариев, есть еще один возможный способ.

Здесь вы проверяете внутри блока и на основе проверки добавляете еще один блок или нет.

    NSOperationQueue * queue = NSOperationQueue.new;

    // Important
    queue.maxConcurrentOperationCount = 1;

    void ( ^ block ) ( void ) = ^ {

        // Whatever you have to do ... do it here
        xxx

        // Perform check
        // Note I run it sync and on the main queue, your requirements may differ
        dispatch_sync ( dispatch_get_main_queue (), ^ {

            // Here the condition is stop or not
            // YES means continue adding blocks
            if ( cond )
            {
                [queue addOperationWithBlock:block];
            }
            // else done

        } );

    };

    // Start it all
    [queue addOperationWithBlock:block];

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

    void ( ^ block1 ) ( void ) = ^ {

        // Some logic
        __block BOOL done = NO;

        while ( ! done )
        {
            // Whatever you have to do ... do it here
            xxx

            // Perform check
            // Note I run it sync and on the main queue, your requirements may differ
            dispatch_sync ( dispatch_get_main_queue (), ^ {

                // Here the condition is stop or not
                // YES means stop! here
                done = cond;

            } );
        }

    };