dispatch_async中にNSOperationQueueを停止する方法

Nov 03 2020

forループのoperationsqueueに多くのブロック操作を追加しています。各操作で、条件が満たされているかどうかを別のスレッドで確認する必要があります。条件が満たされた場合、すべての操作をキャンセルする必要があります。

私はあなたに私の問題を示すためにサンプルコードを作りました:

__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も一時停止= YESもありません。この実行中のスレッドでも完了まで実行される傾向があり、それを停止するロジックが必要なため、これは目的の動作をしません。

また、この例では、メインスレッドの終了条件またはキャンセル条件をそのままにしました。ここでも、これはおそらく何もキャンセルされないことを意味しますが、実際には、通常、ボタンのクリックなどのUIからキャンセルしてから、ここのように実行します。ただし、ロックを使用してどこでもキャンセルできます。

編集

ここにたくさんのコメントに基づいて、別の可能な方法があります。

ここでは、ブロック内をチェックし、チェックに基づいて別のブロックを追加するかどうかを確認します。

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

上記では、毎回同じブロックを使用していますが、これもかなりの前提ですが、簡単に変更して別のブロックを追加できます。ただし、ブロックがすべて同じである場合、必要なのは1つだけであり、新しいブロックのスケジュールを継続する必要はなく、以下のように実行できます。

    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;

            } );
        }

    };