web-dev-qa-db-fra.com

L'annulation d'une tâche génère une exception

D'après ce que j'ai lu sur les tâches, le code suivant devrait annuler la tâche en cours d'exécution sans lever d'exception. J'avais l'impression que le but de l'annulation d'une tâche était de "demander" poliment à la tâche de s'arrêter sans interrompre les discussions.

La sortie du programme suivant est:

Exception de dumping

[OperationCanceledException]

Annulation et retour du dernier prime calculé.

J'essaie d'éviter toute exception lors de l'annulation. Comment puis-je accomplir cela?

void Main()
{
    var cancellationToken = new CancellationTokenSource();

    var task = new Task<int>(() => {
        return CalculatePrime(cancellationToken.Token, 10000);
    }, cancellationToken.Token);

    try
    {
        task.Start();
        Thread.Sleep(100);
        cancellationToken.Cancel();
        task.Wait(cancellationToken.Token);         
    }
    catch (Exception e)
    {
        Console.WriteLine("Dumping exception");
        e.Dump();
    }
}

int CalculatePrime(CancellationToken cancelToken, object digits)
{  
    int factor; 
    int lastPrime = 0;

    int c = (int)digits;

    for (int num = 2; num < c; num++)
    { 
        bool isprime = true;
        factor = 0; 

        if (cancelToken.IsCancellationRequested)
        {
            Console.WriteLine ("Cancelling and returning last calculated prime.");
            //cancelToken.ThrowIfCancellationRequested();
            return lastPrime;
        }

        // see if num is evenly divisible 
        for (int i = 2; i <= num/2; i++)
        { 
            if ((num % i) == 0)
            {             
                // num is evenly divisible -- not prime 
                isprime = false; 
                factor = i; 
            }
        } 

        if (isprime)
        {
            lastPrime = num;
        }
    }

    return lastPrime;
}
52
Vince Panuccio

Vous jetez explicitement une exception sur cette ligne:

cancelToken.ThrowIfCancellationRequested();

Si vous souhaitez quitter gracieusement la tâche, il vous suffit de vous débarrasser de cette ligne.

Généralement, les gens l'utilisent comme mécanisme de contrôle pour s'assurer que le traitement actuel est abandonné sans potentiellement exécuter de code supplémentaire. De plus, il n'est pas nécessaire de vérifier l'annulation lors de l'appel de ThrowIfCancellationRequested() car il est fonctionnellement équivalent à:

if (token.IsCancellationRequested) 
    throw new OperationCanceledException(token);

Lorsque vous utilisez ThrowIfCancellationRequested() votre tâche pourrait ressembler à ceci:

int CalculatePrime(CancellationToken cancelToken, object digits) {
    try{
        while(true){
            cancelToken.ThrowIfCancellationRequested();

            //Long operation here...
        }
    }
    finally{
        //Do some cleanup
    }
}

De plus, Task.Wait(CancellationToken) lèvera une exception si le jeton a été annulé. Pour utiliser cette méthode, vous devrez encapsuler votre appel Wait dans un bloc Try...Catch.

MSDN: comment annuler une tâche

67
Josh

J'essaie d'éviter toute exception lors de l'annulation.

Tu ne devrais pas faire ça.

Lancer OperationCanceledException est la façon idiomatique dont "la méthode que vous avez appelée a été annulée" est exprimée en TPL. Ne vous battez pas contre cela - attendez-vous à cela.

C'est une bonne chose , car cela signifie que lorsque vous avez plusieurs opérations utilisant le même jeton d'annulation, vous n'avez pas besoin de poivrer votre code à chaque niveau avec des vérifications pour voir si la méthode que vous venez d'appeler s'est terminée correctement ou si elle est retournée en raison d'une annulation. Vous pourriez utiliser CancellationToken.IsCancellationRequested partout, mais cela rendra votre code beaucoup moins élégant à long terme.

Notez qu'il y a deux morceaux de code dans votre exemple qui lèvent une exception - une dans la tâche elle-même:

cancelToken.ThrowIfCancellationRequested()

et celui où vous attendez la fin de la tâche:

task.Wait(cancellationToken.Token);

Je ne pense pas que vous souhaitiez vraiment passer le jeton d'annulation dans le task.Wait appelez, pour être honnête ... qui permet autre code d'annuler votre attente . Étant donné que vous savez que vous venez d'annuler ce jeton, il est inutile - il est lié de lever une exception, que la tâche ait déjà remarqué l'annulation ou ne pas. Options:

  • Utilisez un jeton d'annulation différent (afin qu'un autre code puisse annuler votre attente indépendamment)
  • Utiliser un temps mort
  • Attendez aussi longtemps qu'il le faut
87
Jon Skeet

Certaines des réponses ci-dessus se lisent comme si ThrowIfCancellationRequested() serait une option. Il s'agit de pas dans ce cas, car vous n'obtiendrez pas le dernier premier résultat obtenu. Le idiomatic way that "the method you called was cancelled" est défini pour les cas où l'annulation signifie la suppression des résultats (intermédiaires). Si votre définition de l'annulation est "arrêtez le calcul et retournez le dernier résultat intermédiaire", vous êtes déjà parti de cette façon.

Discuter des avantages en particulier en termes d'exécution est également assez trompeur: l'algorithme implémenté est nul à l'exécution. Même une annulation hautement optimisée ne fera aucun bien.

L'optimisation la plus simple serait de dérouler cette boucle et de sauter des cycles inutiles:

for(i=2; i <= num/2; i++) { 
  if((num % i) == 0) { 
    // num is evenly divisible -- not prime 
    isprime = false; 
    factor = i; 
  }
} 

Vous pouvez

  • enregistrer (num/2) -1 cycles pour chaque nombre pair, ce qui est légèrement inférieur à 50% global (déroulement),
  • save (num/2) -square_root_of (num) cycles pour chaque nombre premier (choisissez lié selon les mathématiques du plus petit facteur premier),
  • économiser au moins autant pour chaque non-prime, attendez-vous à beaucoup plus d'économies, par exemple num = 999 se termine par 1 cycle au lieu de 499 (pause, si la réponse est trouvée) et
  • économisez encore 50% de cycles, ce qui est bien sûr 25% global (choisissez l'étape en fonction des mathématiques des nombres premiers, le déroulement gère le cas spécial 2).

Cela revient à économiser un minimum garanti de 75% (estimation approximative: 90%) de cycles dans la boucle intérieure, simplement en la remplaçant par:

if ((num % 2) == 0) {
  isprime = false; 
  factor = 2;
} else {
  for(i=3; i <= (int)Math.sqrt(num); i+=2) { 
    if((num % i) == 0) { 
      // num is evenly divisible -- not prime 
      isprime = false; 
      factor = i;
      break;
    }
  }
} 

Il existe des algorithmes beaucoup plus rapides (dont je ne parlerai pas parce que je suis assez loin du sujet) mais cette optimisation est assez facile et prouve toujours mon point: ne vous inquiétez pas de la micro-optimisation de l'exécution lorsque votre algorithme est this loin d'être optimal.

8
No answer

Une autre remarque sur les avantages d'utiliser ThrowIfCancellationRequested plutôt que IsCancellationRequested: J'ai trouvé que lorsque vous devez utiliser ContinueWith avec une option de continuation de TaskContinuationOptions.OnlyOnCanceled, IsCancellationRequested ne provoquera pas le déclenchement du ContinueWith conditionné. ThrowIfCancellationRequested, cependant, will définit la condition Annulée de la tâche, provoquant le déclenchement de ContinueWith.

Remarque: cela n'est vrai que lorsque la tâche est déjà en cours d'exécution et non lorsque la tâche démarre. C'est pourquoi j'ai ajouté une Thread.Sleep() entre le début et l'annulation.

CancellationTokenSource cts = new CancellationTokenSource();

Task task1 = new Task(() => {
    while(true){
        if(cts.Token.IsCancellationRequested)
            break;
    }
}, cts.Token);
task1.ContinueWith((ant) => {
    // Perform task1 post-cancellation logic.
    // This will NOT fire when calling cst.Cancel().
}

Task task2 = new Task(() => {
    while(true){
        cts.Token.ThrowIfCancellationRequested();
    }
}, cts.Token);
task2.ContinueWith((ant) => {
    // Perform task2 post-cancellation logic.
    // This will fire when calling cst.Cancel().
}

task1.Start();
task2.Start();
Thread.Sleep(3000);
cts.Cancel();
7
Gerard Torres