Come interrompere NSOperationQueue durante dispatch_async

Nov 03 2020

Sto aggiungendo molte operazioni di blocco a una coda di operazioni in un ciclo for. In ogni operazione ho bisogno di controllare su un altro thread se una condizione è soddisfatta. Se la condizione è soddisfatta, tutte le operazioni devono essere annullate.

Ho creato un codice di esempio per mostrarti il ​​mio problema:

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

Output previsto da NSLog:

run: 1

Output che ho ottenuto (varia tra 7 e 9):

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

Ho cercato su Google per ore, ma non sono riuscito a trovare una soluzione.

Risposte

Chris Nov 04 2020 at 00:13

Penso di aver trovato una soluzione. Ecco il codice aggiornato:

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

Fammi usare più spazio. Devi sincronizzare l'accesso alla tua variabile. È l'idea corretta ma la stai usando in modo errato. Hai bisogno di un lucchetto o di un ivar atomico o qualcosa del genere per sincronizzare l'accesso ad esso.

Quindi se annulli nel bit dispatch_async succede looooong dopo che tutti i blocchi sono stati eseguiti. Questo è ciò che mostra il tuo output. Come accennato nel commento, se aggiungi un NSLog es

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

vedrai cosa intendo. Mi aspetto che in genere venga eseguito in profondità nel tuo array o anche dopo che tutto l'array ha terminato l'esecuzione.

Ma il problema più grande è la tua logica. Hai bisogno di una logica per annullare quei blocchi. La semplice messaggistica cancelAllOperationso setSuspendednon è sufficiente ei blocchi già in esecuzione continueranno a funzionare.

Ecco un rapido esempio.

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

Ora questo esempio fa quello che fa il tuo, ma con un po 'più di blocco e un po' più di logica e NO cancelAllOperations né sospeso = YESs. Questo non farà quello che vuoi perché anche con questi thread in esecuzione tendono a funzionare fino al completamento e hai bisogno della logica per fermarlo.

Inoltre, in questo esempio, ho lasciato la condizione di uscita o di annullamento come nel thread principale. Anche in questo caso questo probabilmente significherà che nulla viene cancellato, ma nella vita reale normalmente si annulla da qualche interfaccia utente, ad esempio un clic del pulsante e poi lo si fa come qui. Ma puoi annullare ovunque usando il lucchetto.

MODIFICARE

Sulla base di molti commenti, ecco un altro modo possibile.

Qui controlli all'interno del blocco e in base al controllo aggiungi o meno un altro blocco.

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

Sopra utilizzo lo stesso blocco ogni volta, il che è anche piuttosto un presupposto, ma puoi cambiarlo facilmente per aggiungere blocchi diversi. Tuttavia, se i blocchi sono tutti uguali, ne avrai bisogno solo uno e non è necessario continuare a programmare nuovi blocchi e quindi puoi farlo come di seguito.

    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;

            } );
        }

    };