Bagaimana cara membatalkan Tugas dalam menunggu?

164

Saya bermain dengan tugas-tugas Windows 8 WinRT ini, dan saya mencoba untuk membatalkan tugas menggunakan metode di bawah ini, dan itu berfungsi sampai titik tertentu. Metode CancelNotification TIDAK dipanggil, yang membuat Anda berpikir tugas itu dibatalkan, tetapi di latar belakang tugas itu terus berjalan, kemudian setelah selesai, status Tugas selalu selesai dan tidak pernah dibatalkan. Apakah ada cara untuk benar-benar menghentikan tugas saat dibatalkan?

private async void TryTask()
{
    CancellationTokenSource source = new CancellationTokenSource();
    source.Token.Register(CancelNotification);
    source.CancelAfter(TimeSpan.FromSeconds(1));
    var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token);

    await task;            

    if (task.IsCompleted)
    {
        MessageDialog md = new MessageDialog(task.Result.ToString());
        await md.ShowAsync();
    }
    else
    {
        MessageDialog md = new MessageDialog("Uncompleted");
        await md.ShowAsync();
    }
}

private int slowFunc(int a, int b)
{
    string someString = string.Empty;
    for (int i = 0; i < 200000; i++)
    {
        someString += "a";
    }

    return a + b;
}

private void CancelNotification()
{
}
Carlo
sumber
Baru saja menemukan artikel ini yang membantu saya memahami berbagai cara untuk membatalkan.
Uwe Keim

Jawaban:

239

Baca di Pembatalan (yang diperkenalkan di NET 4.0 dan sebagian besar tidak berubah sejak saat itu) dan Pola Asynchronous Task-Based , yang menyediakan panduan tentang bagaimana menggunakan CancellationTokendengan asyncmetode.

Untuk meringkas, Anda meneruskan CancellationTokenke setiap metode yang mendukung pembatalan, dan metode itu harus memeriksanya secara berkala.

private async Task TryTask()
{
  CancellationTokenSource source = new CancellationTokenSource();
  source.CancelAfter(TimeSpan.FromSeconds(1));
  Task<int> task = Task.Run(() => slowFunc(1, 2, source.Token), source.Token);

  // (A canceled task will raise an exception when awaited).
  await task;
}

private int slowFunc(int a, int b, CancellationToken cancellationToken)
{
  string someString = string.Empty;
  for (int i = 0; i < 200000; i++)
  {
    someString += "a";
    if (i % 1000 == 0)
      cancellationToken.ThrowIfCancellationRequested();
  }

  return a + b;
}
Stephen Cleary
sumber
2
Wah info bagus! Itu bekerja dengan sempurna, sekarang saya perlu mencari cara untuk menangani pengecualian dalam metode async. Terima kasih sobat! Saya akan membaca hal-hal yang Anda sarankan.
Carlo
8
Tidak. Sebagian besar metode sinkronisasi yang sudah berjalan lama memiliki beberapa cara untuk membatalkannya - kadang-kadang dengan menutup sumber daya yang mendasarinya atau memanggil metode lain. CancellationTokenmemiliki semua kait yang diperlukan untuk melakukan interop dengan sistem pembatalan kustom, tetapi tidak ada yang dapat membatalkan metode yang tidak dibatalkan.
Stephen Cleary
1
Ah saya mengerti. Jadi cara terbaik untuk menangkap ProcessCancelledException adalah dengan membungkus 'menunggu' dengan mencoba / menangkap? Kadang-kadang saya mendapatkan AggregatedException dan saya tidak bisa mengatasinya.
Carlo
3
Baik. Saya sarankan Anda tidak pernah menggunakan Waitatau Resultdalam asyncmetode; Anda harus selalu menggunakan await, yang membuka pengecualian dengan benar.
Stephen Cleary
11
Hanya ingin tahu, apakah ada alasan mengapa tidak ada contoh yang digunakan CancellationToken.IsCancellationRequesteddan malah menyarankan untuk melemparkan pengecualian?
James M
41

Atau, untuk menghindari modifikasi slowFunc(misalnya Anda tidak memiliki akses ke kode sumber):

var source = new CancellationTokenSource(); //original code
source.Token.Register(CancelNotification); //original code
source.CancelAfter(TimeSpan.FromSeconds(1)); //original code
var completionSource = new TaskCompletionSource<object>(); //New code
source.Token.Register(() => completionSource.TrySetCanceled()); //New code
var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token); //original code

//original code: await task;  
await Task.WhenAny(task, completionSource.Task); //New code

Anda juga dapat menggunakan metode ekstensi yang bagus dari https://github.com/StephenCleary/AsyncEx dan membuatnya terlihat sesederhana:

await Task.WhenAny(task, source.Token.AsTask());
sonatique
sumber
1
Ini terlihat sangat rumit ... secara keseluruhan async-tunggu implementasi. Saya tidak berpikir bahwa konstruksi seperti itu membuat kode sumber lebih mudah dibaca.
Maxim
1
Terima kasih, satu token registrasi catatan harus dibuang nanti, hal kedua - gunakan ConfigureAwaitjika tidak Anda bisa terluka di aplikasi UI.
astrowalker
@astrowalker: ya memang pendaftaran token sebaiknya dibatalkan registrasi (dibuang). Ini dapat dilakukan di dalam delegasi yang diteruskan ke Register () dengan memanggil buang objek yang dikembalikan oleh Register (). Namun karena token "sumber" hanya bersifat lokal dalam hal ini, semuanya akan tetap dihapus ...
sonatique
1
Sebenarnya yang dibutuhkan hanyalah sarangnya using.
astrowalker
@astrowalker ;-) ya Anda benar sebenarnya. Dalam hal ini, ini adalah solusi yang lebih sederhana! Namun, jika Anda ingin mengembalikan Task.WhenAny langsung (tanpa menunggu) maka Anda memerlukan sesuatu yang lain. Saya mengatakan ini karena saya pernah mengalami masalah refactoring seperti ini: sebelum saya menggunakan ... tunggu. Saya kemudian menghapus menunggu (dan async pada fungsi) karena hanya itu satu-satunya, tanpa memperhatikan bahwa saya benar-benar melanggar kode. Bug yang dihasilkan sulit ditemukan. Jadi saya enggan menggunakan using () bersama dengan async / menunggu. Saya merasa pola Buang tidak cocok dengan hal-hal async ...
sonatique
15

Satu kasus yang belum dibahas adalah bagaimana menangani pembatalan di dalam metode async. Ambil contoh kasus sederhana di mana Anda perlu mengunggah beberapa data ke layanan untuk menghitung sesuatu dan kemudian mengembalikan beberapa hasil.

public async Task<Results> ProcessDataAsync(MyData data)
{
    var client = await GetClientAsync();
    await client.UploadDataAsync(data);
    await client.CalculateAsync();
    return await client.GetResultsAsync();
}

Jika Anda ingin mendukung pembatalan, maka cara termudah adalah dengan memberikan token dan memeriksa apakah telah dibatalkan di antara setiap pemanggilan metode async (atau menggunakan ContinueWith). Jika panggilannya sangat lama, Anda bisa menunggu beberapa saat untuk membatalkan. Saya membuat metode pembantu kecil sebagai gantinya gagal segera setelah dibatalkan.

public static class TaskExtensions
{
    public static async Task<T> WaitOrCancel<T>(this Task<T> task, CancellationToken token)
    {
        token.ThrowIfCancellationRequested();
        await Task.WhenAny(task, token.WhenCanceled());
        token.ThrowIfCancellationRequested();

        return await task;
    }

    public static Task WhenCanceled(this CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
        return tcs.Task;
    }
}

Jadi untuk menggunakannya, tambahkan saja .WaitOrCancel(token)ke panggilan async:

public async Task<Results> ProcessDataAsync(MyData data, CancellationToken token)
{
    Client client;
    try
    {
        client = await GetClientAsync().WaitOrCancel(token);
        await client.UploadDataAsync(data).WaitOrCancel(token);
        await client.CalculateAsync().WaitOrCancel(token);
        return await client.GetResultsAsync().WaitOrCancel(token);
    }
    catch (OperationCanceledException)
    {
        if (client != null)
            await client.CancelAsync();
        throw;
    }
}

Perhatikan bahwa ini tidak akan menghentikan Tugas yang Anda tunggu dan itu akan terus berjalan. Anda harus menggunakan mekanisme yang berbeda untuk menghentikannya, seperti CancelAsyncpanggilan dalam contoh, atau lebih baik lagi lulus dalam yang sama CancellationTokendengan Tasksehingga dapat menangani pembatalan akhirnya. Mencoba untuk membatalkan utas tidak disarankan .

kjbartel
sumber
1
Perhatikan bahwa sementara ini membatalkan menunggu tugas, itu tidak membatalkan tugas yang sebenarnya (jadi mis. UploadDataAsyncDapat melanjutkan di latar belakang, tetapi setelah selesai itu tidak akan membuat panggilan ke CalculateAsynckarena bagian itu sudah berhenti menunggu). Ini mungkin atau mungkin tidak bermasalah bagi Anda, terutama jika Anda ingin mencoba kembali operasi. Melewati CancellationTokensemua jalan adalah pilihan yang lebih disukai, bila memungkinkan.
Miral
1
@ Miral itu benar namun ada banyak metode async yang tidak mengambil token pembatalan. Ambil contoh layanan WCF, yang ketika Anda menghasilkan klien dengan metode Async tidak akan menyertakan token pembatalan. Memang seperti yang ditunjukkan contoh, dan seperti yang dicatat oleh Stephen Cleary, diasumsikan bahwa tugas sinkron yang berjalan lama memiliki beberapa cara untuk membatalkannya.
kjbartel
1
Itu sebabnya saya mengatakan "bila mungkin". Sebagian besar saya hanya ingin peringatan ini disebutkan sehingga orang yang menemukan jawaban ini nanti tidak mendapatkan kesan yang salah.
Miral
@ Miral, terima kasih. Saya telah memperbarui untuk mencerminkan peringatan ini.
kjbartel
Sayangnya ini tidak berfungsi dengan metode seperti 'NetworkStream.WriteAsync'.
Zeokat
6

Saya hanya ingin menambahkan jawaban yang sudah diterima. Saya terjebak dalam hal ini, tetapi saya mengambil rute yang berbeda dalam menangani acara lengkap. Daripada menjalankan menunggu, saya menambahkan penangan yang selesai untuk tugas.

Comments.AsAsyncAction().Completed += new AsyncActionCompletedHandler(CommentLoadComplete);

Di mana event handler terlihat seperti ini

private void CommentLoadComplete(IAsyncAction sender, AsyncStatus status )
{
    if (status == AsyncStatus.Canceled)
    {
        return;
    }
    CommentsItemsControl.ItemsSource = Comments.Result;
    CommentScrollViewer.ScrollToVerticalOffset(0);
    CommentScrollViewer.Visibility = Visibility.Visible;
    CommentProgressRing.Visibility = Visibility.Collapsed;
}

Dengan rute ini, semua penanganan sudah dilakukan untuk Anda, ketika tugas dibatalkan itu hanya memicu pengendali acara dan Anda dapat melihat apakah itu dibatalkan di sana.

Smeegs
sumber