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

#objective-c

#objective-c

Вопрос:

Я добавляю много блочных операций в очередь операций в цикле 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
  

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

Комментарии:

1. Вам нужно синхронизировать доступ queueDidCancel , но это не проблема. Очередь отменяется, но слишком поздно, так как к тому времени все блоки завершили выполнение. Просто добавьте NSLog, в котором вы отменяете очередь, которая регистрирует что-то вроде «Отменено», и вы поймете, что я имею в виду. Но самая большая проблема заключается в том, что даже если вы все сделаете правильно, это не обязательно отменит выполнение задач. Для этого вам нужно немного больше логики.

2. @skaak Вы правы. Очередь отменяется, но слишком поздно. Кстати. Мне не нужно останавливать текущую выполняемую операцию. Я только хочу запретить OperationQueue запускать выполнение следующих операций.

3. Это [myQueue setSuspended: true] . Предотвращение планирования очереди называется приостановкой, а не отменой. Очереди не могут быть отменены.

4. @RobNapier Я думаю, здесь нам нужно и то, и другое — остановить запущенные, а также предотвратить планирование новых. Это то, что я сделал в своем примере, используя простое, хотя и синхронизированное логическое значение. Это логическое значение предотвращает планирование новых блоков, а также приводит к прекращению выполнения запущенных блоков.

5. Крис, ты можешь отредактировать свой вопрос и дать нам представление о том, что делают эти десять операций? Правильное решение зависит от выполнения этих операций…

Ответ №1:

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

 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];
            }
        });
    }];
}
  

Комментарии:

1. Хорошо, но это игрушечный пример. Первый блок просто приостанавливает очередь. Остальные все еще добавляются, но из-за приостановки не выполняются. Затем в основном потоке позже вы продолжаете его запускать. Это не имеет смысла для того, что вы хотите, если вы не хотите, чтобы кнопка продолжения не была отменена? Вы хотите, чтобы (много) вещей выполнялись в задней части, а затем отменяли их позже в main. Я думаю, что мой ответ работает здесь, поскольку он выполняет оба — он предотвращает запуск любых блоков, которые могут выполняться в настоящее время, и предотвращает добавление любых новых блоков. Это то, что вы хотите сделать правильно?

2. Еще один комментарий — в моем примере используется блокировка для синхронизации логического значения, но в классе вы также можете использовать атомарный ivar. Затем вы можете просто получить и установить его по своему усмотрению, поскольку ОС синхронизирует доступ к нему.

3. Крис, мне не ясно, какую проблему это решает. Что, если @"1" это не первая строка в массиве. Этот код просто остановится. Это то, чего вы пытались достичь?

4. @skaak да, каждая операция приостанавливает очередь в начале выполнения. Как я нашел в документах, текущая операция будет завершена (это то, что я хочу), даже когда очередь приостановлена. И когда блок dispatch_async завершен, он может решить возобновить очередь или нет. Вы правы, это похоже на кнопку продолжения, а не на кнопку отмены. Но речь идет не о кнопке для остановки. Речь идет о том, чтобы сделать что-то в другом потоке (не в пользовательском интерфейсе) и, основываясь на результате, остановить выполнение. Я должен был быть более ясным в описании моей проблемы.

5. Хорошо, это в основном то, как я это понял… это немного отличается тем, что вы хотите , чтобы выполняемая операция выполнялась до завершения. Я думал, вы хотите отменить все запущенные, когда произойдет событие (например, щелчок). Тогда вы действительно можете использовать приостановку или отмену. Еще один вопрос — будет ли у вас когда-либо выполняться более одной операции в любой момент времени?

Ответ №2:

Позвольте мне использовать больше места. Вам необходимо синхронизировать доступ к вашей переменной. Это правильная идея, но вы используете ее неправильно. Вам нужна блокировка или атомарный 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];
            }
        });
    }];
}
  

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

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

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

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

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

     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;

            } );
        }

    };
  

Комментарии:

1. Это воспроизводит всю эту queueDidCancel логику из исходного сообщения, которое следует удалить.

2. Он сохраняет логику, добавляет к ней и синхронизирует ее, а также показывает, как использовать ее в типичной среде пользовательского интерфейса. Если вы удалите его, то в тот момент, когда вы переходите из последовательной очереди в параллельную, вы переписываете все это снова. Это работает для последовательной и параллельной очереди, для отмены пользовательского интерфейса или фона и т. Д.

3. Отмена довольно проста (и последовательный против параллельного не имеет значения). Когда вы отменяете операцию перед ее запуском, она просто не запускается. Если вы хотите отменить операции, которые, возможно, уже начались, вам необходимо периодически проверять isCancelled , пока выполняется длительная операция (или, если подкласс асинхронной операции, иногда переопределяет cancel реализацию). Но добавление OP еще одного логического значения поверх существующего логического значения, которое уже предоставлено, является плохим дизайном.

4. @skaak спасибо за разъяснения, но нет ли способа предотвратить выполнение следующей операции до завершения dispatch_async?

5. Вы могли бы использовать dispatch_sync — но есть много способов. Не совсем уверен, что вы хотите сделать?