Làm thế nào để hủy bỏ một nhiệm vụ đang chờ?


164

Tôi đang chơi với các tác vụ WinRT Windows 8 này và tôi đang cố gắng hủy tác vụ bằng phương pháp bên dưới và nó hoạt động đến một lúc nào đó. Phương thức Hủy bỏ thông báo KHÔNG được gọi, khiến bạn nghĩ rằng tác vụ đã bị hủy, nhưng trong nền, tác vụ vẫn tiếp tục chạy, sau khi hoàn thành, trạng thái của Tác vụ luôn hoàn thành và không bao giờ bị hủy. Có cách nào để dừng hoàn toàn nhiệm vụ khi nó bị hủy không?

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()
{
}

Chỉ cần tìm thấy bài viết này đã giúp tôi hiểu các cách khác nhau để hủy bỏ.
Uwe Keim

Câu trả lời:


239

Đọc về Hủy bỏ (được giới thiệu trong .NET 4.0 và phần lớn không thay đổi kể từ đó) và Mẫu không đồng bộ dựa trên nhiệm vụ , cung cấp hướng dẫn về cách sử dụng CancellationTokenvới asynccác phương thức.

Để tóm tắt, bạn chuyển một CancellationTokenvào từng phương thức hỗ trợ hủy bỏ và phương thức đó phải kiểm tra định kỳ.

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

2
Wow thông tin tuyệt vời! Điều đó đã làm việc hoàn hảo, bây giờ tôi cần tìm ra cách xử lý ngoại lệ trong phương thức async. Cảm ơn người đàn ông! Tôi sẽ đọc những thứ bạn đề nghị.
Carlo

8
Không. Hầu hết các phương thức đồng bộ chạy dài có một số cách để hủy chúng - đôi khi bằng cách đóng tài nguyên cơ bản hoặc gọi một phương thức khác. CancellationTokencó tất cả các móc cần thiết để can thiệp với các hệ thống hủy tùy chỉnh, nhưng không có gì có thể hủy bỏ một phương thức không thể kiểm soát được.
Stephen Cleary

1
Ah tôi thấy. Vì vậy, cách tốt nhất để bắt ProcessCancellingException là bằng cách gói 'await' trong một thử / bắt? Đôi khi tôi nhận được AggregatedException và tôi không thể xử lý điều đó.
Carlo

3
Đúng. Tôi khuyên bạn không bao giờ nên sử dụng Waithoặc Resulttrong asynccác phương pháp; awaitthay vào đó, bạn nên luôn luôn sử dụng ngoại lệ một cách chính xác.
Stephen Cleary

11
Chỉ tò mò, có một lý do tại sao không có ví dụ nào sử dụng CancellationToken.IsCancellationRequestedvà thay vào đó đề nghị ném ngoại lệ?
James M

41

Hoặc, để tránh sửa đổi slowFunc(ví dụ: bạn không có quyền truy cập vào mã nguồn):

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

Bạn cũng có thể sử dụng các phương thức tiện ích mở rộng đẹp từ https://github.com/StephenCleary/AsyncEx và có giao diện đơn giản như sau:

await Task.WhenAny(task, source.Token.AsTask());

1
Nó trông rất phức tạp ... như toàn bộ async - đang chờ triển khai. Tôi không nghĩ rằng các cấu trúc như vậy làm cho mã nguồn dễ đọc hơn.
Maxim

1
Cảm ơn bạn, một lưu ý - mã thông báo đăng ký nên được xử lý sau, điều thứ hai - sử dụng ConfigureAwaitnếu không bạn có thể bị tổn thương trong các ứng dụng UI.
Astrowalker

@astrowalker: có thực sự đăng ký mã thông báo sẽ tốt hơn nếu không đăng ký (xử lý). Điều này có thể được thực hiện bên trong đại biểu được chuyển đến Đăng ký () bằng cách gọi xử lý trên đối tượng được trả về bởi Đăng ký (). Tuy nhiên, vì mã thông báo "nguồn" chỉ là cục bộ trong trường hợp này, mọi thứ sẽ bị xóa bằng mọi cách ...
sonatique

1
Trên thực tế tất cả những gì nó cần là làm tổ using.
Astrowalker 15/03/18

@astrowalker ;-) vâng, bạn thực sự đúng. Trong trường hợp này đây là giải pháp đơn giản hơn nhiều! Tuy nhiên, nếu bạn muốn trả lại Task.WhenAny trực tiếp (không phải chờ đợi) thì bạn cần một cái gì đó khác. Tôi nói điều này bởi vì tôi đã từng gặp phải một vấn đề tái cấu trúc như thế này: trước khi tôi sử dụng ... chờ đợi. Sau đó tôi đã xóa await (và async trên hàm) vì nó là duy nhất, mà không nhận thấy rằng tôi đã phá vỡ hoàn toàn mã. Các lỗi kết quả là khó tìm. Do đó, tôi miễn cưỡng sử dụng bằng cách sử dụng () cùng với async / await. Tôi cảm thấy mô hình Vứt bỏ không phù hợp với những thứ không đồng bộ ...
sonatique 15/03/18

15

Một trường hợp chưa được đề cập là cách xử lý hủy bên trong phương thức không đồng bộ. Lấy ví dụ một trường hợp đơn giản trong đó bạn cần tải một số dữ liệu lên một dịch vụ để lấy nó để tính toán một cái gì đó và sau đó trả về một số kết quả.

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

Nếu bạn muốn hỗ trợ hủy thì cách dễ nhất là chuyển mã thông báo và kiểm tra xem nó có bị hủy giữa mỗi lần gọi phương thức không đồng bộ không (hoặc sử dụng ContinWith). Nếu chúng là các cuộc gọi chạy rất lâu mặc dù bạn có thể đợi một lúc để hủy. Tôi đã tạo ra một phương thức trợ giúp nhỏ để thay vì thất bại ngay khi hủy bỏ.

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

Vì vậy, để sử dụng nó, sau đó chỉ cần thêm .WaitOrCancel(token)vào bất kỳ cuộc gọi async nào:

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

Lưu ý rằng điều này sẽ không dừng Nhiệm vụ mà bạn đang chờ và nó sẽ tiếp tục chạy. Bạn sẽ cần sử dụng một cơ chế khác để ngăn chặn nó, chẳng hạn như CancelAsynccuộc gọi trong ví dụ hoặc tốt hơn là chuyển qua cùng CancellationTokenđể Taskcó thể xử lý hủy cuối cùng. Cố gắng hủy bỏ chủ đề không được khuyến khích .


1
Lưu ý rằng trong khi điều này hủy bỏ chờ đợi cho nhiệm vụ, nó sẽ không hủy tác vụ thực tế (ví dụ: UploadDataAsynccó thể tiếp tục ở chế độ nền, nhưng một khi nó hoàn thành, nó sẽ không thực hiện cuộc gọi CalculateAsyncvì phần đó đã dừng chờ). Điều này có thể hoặc không thể gây rắc rối cho bạn, đặc biệt nếu bạn muốn thử lại thao tác. Vượt qua CancellationTokentất cả các cách là tùy chọn ưa thích, khi có thể.
Miral

1
@Mirus đó là sự thật tuy nhiên có nhiều phương thức không đồng bộ không thực hiện mã thông báo hủy. Lấy ví dụ về các dịch vụ WCF, khi bạn tạo ứng dụng khách bằng các phương thức Async sẽ không bao gồm các mã thông báo hủy. Quả thực như ví dụ cho thấy, và như Stephen Cleary cũng lưu ý, người ta cho rằng các tác vụ đồng bộ chạy dài có một số cách để hủy chúng.
kjbartel

1
Đó là lý do tại sao tôi nói "khi có thể". Hầu như tôi chỉ muốn cảnh báo này được đề cập để mọi người tìm thấy câu trả lời này sau đó không có ấn tượng sai.
Miral

@Mirus Cảm ơn. Tôi đã cập nhật để phản ánh sự cảnh báo này.
kjbartel

Đáng buồn là điều này không hoạt động với các phương thức như 'NetworkStream.WriteAsync'.
Zeokat

6

Tôi chỉ muốn thêm vào câu trả lời đã được chấp nhận. Tôi đã bị mắc kẹt trong vấn đề này, nhưng tôi đã đi một con đường khác để xử lý sự kiện hoàn chỉnh. Thay vì chạy chờ, tôi thêm một trình xử lý đã hoàn thành vào nhiệm vụ.

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

Trường hợp xử lý sự kiện trông như thế này

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

Với tuyến đường này, tất cả việc xử lý đã được thực hiện cho bạn, khi tác vụ bị hủy, nó chỉ kích hoạt trình xử lý sự kiện và bạn có thể xem liệu nó có bị hủy ở đó không.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.