web-dev-qa-db-fra.com

Gérez avec élégance l'annulation des tâches

Lorsque j'utilise des tâches pour des charges de travail importantes/longues que je dois pouvoir annuler, j'utilise souvent un modèle similaire à celui-ci pour l'action que la tâche exécute:

public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (OperationCanceledException)
    {
        throw;
    }
    catch (Exception ex)
    {
        Log.Exception(ex);
        throw;
    }
}

L'opération OperationCanceledException ne doit pas être enregistrée en tant qu'erreur mais ne doit pas être avalée si la tâche doit passer à l'état annulé. Aucune autre exception n'a besoin d'être traitée au-delà de la portée de cette méthode.

Cela a toujours semblé un peu maladroit, et Visual Studio par défaut interrompra le lancement d'OperationCanceledException (bien que la fonction "Interruption sur l'utilisateur non géré" soit désormais désactivée pour OperationCanceledException en raison de mon utilisation de ce modèle).

Idéalement, je pense que j'aimerais pouvoir faire quelque chose comme ça:

public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (Exception ex) exclude (OperationCanceledException)
    {
        Log.Exception(ex);
        throw;
    }
}

c'est-à-dire avoir une sorte de liste d'exclusion appliquée à la capture mais sans prise en charge de la langue qui n'est pas actuellement possible (@ eric-lippert: c # vNext feature :)).

Une autre façon serait de poursuivre:

public void StartWork()
{
    Task.Factory.StartNew(() => DoWork(cancellationSource.Token), cancellationSource.Token)
        .ContinueWith(t => Log.Exception(t.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
}

public void DoWork(CancellationToken cancelToken)
{
    //do work
    cancelToken.ThrowIfCancellationRequested();
    //more work
}

mais je n'aime pas vraiment cela, car l'exception pourrait techniquement avoir plus d'une seule exception interne et vous n'avez pas autant de contexte lors de la journalisation de l'exception que dans le premier exemple (si je faisais plus que simplement la journaliser) ).

Je comprends que c'est un peu une question de style, mais vous vous demandez si quelqu'un a de meilleures suggestions?

Dois-je simplement m'en tenir à l'exemple 1?

Eamon

31
Eamon

Donc quel est le problème? Jetez simplement le bloc catch (OperationCanceledException) et définissez les suites appropriées:

var cts = new CancellationTokenSource();
var task = Task.Factory.StartNew(() =>
    {
        var i = 0;
        try
        {
            while (true)
            {
                Thread.Sleep(1000);

                cts.Token.ThrowIfCancellationRequested();

                i++;

                if (i > 5)
                    throw new InvalidOperationException();
            }
        }
        catch
        {
            Console.WriteLine("i = {0}", i);
            throw;
        }
    }, cts.Token);

task.ContinueWith(t => 
        Console.WriteLine("{0} with {1}: {2}", 
            t.Status, 
            t.Exception.InnerExceptions[0].GetType(), 
            t.Exception.InnerExceptions[0].Message
        ), 
        TaskContinuationOptions.OnlyOnFaulted);

task.ContinueWith(t => 
        Console.WriteLine(t.Status), 
        TaskContinuationOptions.OnlyOnCanceled);

Console.ReadLine();

cts.Cancel();

Console.ReadLine();

TPL distingue l'annulation et la faute. Par conséquent, l'annulation (c'est-à-dire le lancement de OperationCancelledException dans le corps de la tâche) n'est pas une faute .

Le point principal: ne pas faire gérer les exceptions dans le corps de la tâche sans les relancer.

15
Dennis

Voici comment vous gérez élégamment l'annulation des tâches:

Gérer les tâches "tirer et oublier"

var cts = new CancellationTokenSource( 5000 );  // auto-cancel in 5 sec.
Task.Run( () => {
    cts.Token.ThrowIfCancellationRequested();

    // do background work

    cts.Token.ThrowIfCancellationRequested();

    // more work

}, cts.Token ).ContinueWith( task => {
    if ( !task.IsCanceled && task.IsFaulted )   // suppress cancel exception
        Logger.Log( task.Exception );           // log others
} );

Traitement en attente d'achèvement/annulation de la tâche

var cts = new CancellationTokenSource( 5000 ); // auto-cancel in 5 sec.
var taskToCancel = Task.Delay( 10000, cts.Token );  

// do work

try { await taskToCancel; }           // await cancellation
catch ( OperationCanceledException ) {}    // suppress cancel exception, re-throw others
9
Casey Anderson

C # 6.0 a une solution pour cela .. exception de filtrage

int denom;

try
{
     denom = 0;
    int x = 5 / denom;
}

// Catch /0 on all days but Saturday

catch (DivideByZeroException xx) when (DateTime.Now.DayOfWeek != DayOfWeek.Saturday)
{
     Console.WriteLine(xx);
}
6
user5628548

Selon cet article de blog MSDN , vous devez intercepter OperationCanceledException, par exemple.

async Task UserSubmitClickAsync(CancellationToken cancellationToken)
{
   try
   {
      await SendResultAsync(cancellationToken);
   }
   catch (OperationCanceledException) // includes TaskCanceledException
   {
      MessageBox.Show(“Your submission was canceled.”);
   }
}

Si votre méthode annulable se situe entre d'autres opérations annulables, vous devrez peut-être effectuer un nettoyage lors de l'annulation. Ce faisant, vous pouvez utiliser le bloc catch ci-dessus, mais assurez-vous de relancer correctement:

async Task SendResultAsync(CancellationToken cancellationToken)
{
   try
   {
      await httpClient.SendAsync(form, cancellationToken);
   }
   catch (OperationCanceledException)
   {
      // perform your cleanup
      form.Dispose();

      // rethrow exception so caller knows you’ve canceled.
      // DON’T “throw ex;” because that stomps on 
      // the Exception.StackTrace property.
      throw; 
   }
}
1
Bondolin

Vous pouvez faire quelque chose comme ça:

public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (OperationCanceledException) when (cancelToken.IsCancellationRequested)
    {
        throw;
    }
    catch (Exception ex)
    {
        Log.Exception(ex);
        throw;
    }
}
0
Jesper Meyer