2014-06-23 21 views
16

Thông thường tôi không đăng câu hỏi với câu trả lời, nhưng lần này tôi muốn thu hút sự chú ý đến những gì tôi nghĩ có thể là một vấn đề không rõ ràng. Nó được kích hoạt bởi this question, kể từ đó tôi xem xét mã cũ của riêng mình và thấy một số mã bị ảnh hưởng bởi điều này.Trạng thái tác vụ bị lỗi và bị hủy sau khi hủy bỏ.ThrowIfCancellationRequested

Mã bên dưới bắt đầu và đang chờ hai tác vụ, task1task2, hầu như giống hệt nhau. task1 chỉ khác với task2 ở chỗ nó chạy một vòng lặp không bao giờ kết thúc. IMO, cả hai trường hợp đều khá điển hình đối với một số tình huống thực tế trong cuộc sống thực hiện công việc liên kết CPU.

using System; 
using System.Threading; 
using System.Threading.Tasks; 

namespace ConsoleApplication 
{ 
    public class Program 
    { 
     static async Task TestAsync() 
     { 
      var ct = new CancellationTokenSource(millisecondsDelay: 1000); 
      var token = ct.Token; 

      // start task1 
      var task1 = Task.Run(() => 
      { 
       for (var i = 0; ; i++) 
       { 
        Thread.Sleep(i); // simulate work item #i 
        token.ThrowIfCancellationRequested(); 
       } 
      }); 

      // start task2 
      var task2 = Task.Run(() => 
      { 
       for (var i = 0; i < 1000; i++) 
       { 
        Thread.Sleep(i); // simulate work item #i 
        token.ThrowIfCancellationRequested(); 
       } 
      }); 

      // await task1 
      try 
      { 
       await task1; 
      } 
      catch (Exception ex) 
      { 
       Console.WriteLine(new { task = "task1", ex.Message, task1.Status }); 
      } 

      // await task2 
      try 
      { 
       await task2; 
      } 
      catch (Exception ex) 
      { 
       Console.WriteLine(new { task = "task2", ex.Message, task2.Status }); 
      } 
     } 

     public static void Main(string[] args) 
     { 
      TestAsync().Wait(); 
      Console.WriteLine("Enter to exit..."); 
      Console.ReadLine(); 
     } 
    } 
} 

Fiddle is here. Kết quả:

 
{ task = task1, Message = The operation was canceled., Status = Canceled } 
{ task = task2, Message = The operation was canceled., Status = Faulted } 

Tại sao tình trạng của task1Cancelled, nhưng tình trạng của task2Faulted? Lưu ý, trong cả hai trường hợp, tôi làm không vượt qua token làm thông số thứ 2 cho Task.Run.

+1

Tôi rất vui vì bài viết của tôi bị khiêu khích câu hỏi của bạn. – i3arnon

+1

@ l3arnon, đó thực sự là một bài đăng tuyệt vời, cả hai đều trên thực tế. – Noseratio

Trả lời

10

Có hai vấn đề ở đây. Trước tiên, bạn nên chuyển CancellationToken thành API Task.Run, ngoài việc cung cấp API cho lambda của công việc. Làm như vậy liên kết mã thông báo với nhiệm vụ và là yếu tố quan trọng để tuyên truyền chính xác việc hủy được kích hoạt bởi token.ThrowIfCancellationRequested.

Tuy nhiên, điều này không giải thích lý do tại sao trạng thái hủy cho task1 vẫn được truyền chính xác (task1.Status == TaskStatus.Canceled), trong khi nó không cho task2 (task2.Status == TaskStatus.Faulted).

Bây giờ, đây có thể là một trong những trường hợp rất hiếm hoi mà logic suy luận loại C# thông minh có thể chống lại ý muốn của nhà phát triển. Nó được thảo luận chi tiết tuyệt vời herehere. Tóm lại, trong trường hợp với task1, ghi đè sau Task.Run được suy ra bởi trình biên dịch:

public static Task Run(Func<Task> function) 

hơn:

public static Task Run(Action action) 

Đó là bởi vì task1 lambda không có đường dẫn mã tự nhiên ra khỏi for vòng lặp, vì vậy nó cũng có thể là một lambda Func<Task>, mặc dù nó không phải là async và nó không trả lại bất cứ điều gì. Đây là tùy chọn mà trình biên dịch ưu tiên hơn Action. Sau đó, việc sử dụng ghi đè như vậy Task.Run tương đương với điều này:

var task1 = Task.Factory.StartNew(new Func<Task>(() => 
{ 
    for (var i = 0; ; i++) 
    { 
     Thread.Sleep(i); // simulate work item #i 
     token.ThrowIfCancellationRequested(); 
    } 
})).Unwrap(); 

Một nhiệm vụ lồng nhau của các loại Task<Task> được trả về bởi Task.Factory.StartNew, mà được unwrapped-Task bởi Unwrap(). Task.Runis smart enough để tự động mở như vậy khi nó chấp nhận Func<Task>. Tác vụ kiểu lời hứa chưa được mở một cách chính xác tuyên truyền trạng thái hủy từ nhiệm vụ bên trong của nó, được ném thành một ngoại lệ OperationCanceledException bởi lambda Func<Task>. Điều này không xảy ra cho task2, chấp nhận một lambda Action và không tạo bất kỳ tác vụ bên trong nào.Việc hủy không được tuyên truyền cho task2, bởi vì token chưa được liên kết với task2 qua Task.Run.

Cuối cùng, đây có thể là hành vi mong muốn cho task1 (chắc chắn không phải cho task2), nhưng chúng tôi không muốn tạo các tác vụ lồng nhau phía sau cảnh trong cả hai trường hợp. Hơn nữa, hành vi này cho task1 có thể dễ dàng bị hỏng bằng cách giới thiệu một điều kiện break ra khỏi vòng lặp for.

Mã chính xác cho task1 nên này:

var task1 = Task.Run(new Action(() => 
{ 
    for (var i = 0; ; i++) 
    { 
     Thread.Sleep(i); // simulate work item #i 
     token.ThrowIfCancellationRequested(); 
    } 
}), token); 
+0

Vì vậy, khi một mã thông báo được ngầm truyền 'Task.Run' thay vì một cách rõ ràng thông qua một tham số, nó được coi là mọi ngoại lệ khác đã xảy ra bên trong lambda? Tôi đã thử duyệt qua đăng ký CancellationToken nhưng không thể tìm ra nhiều trong số đó trong mã nguồn –

+0

@YuvalItzchakov, điều đó đúng với 'Task.Run'. Tuy nhiên, đối với một phương thức 'async Task' đơn giản, phép thuật được thực hiện bằng mã cơ sở hạ tầng trình biên dịch 'async/await':' async Task TestAsync (token CancellationToken) {token.ThrowIfCancellationRequested(); } 'sẽ trả về một' Task' với 'task.IsCanceled == true', nếu hủy bỏ yêu cầu trên token. – Noseratio

+0

Điều đó thật thú vị. Tôi tự hỏi tại sao họ cư xử khác đi. Liệu một lambda không đồng bộ có hoạt động như 'task1' hay' task2'? hoặc nó sẽ khác nhau tùy thuộc vào loại đại biểu trả lại? –

Các vấn đề liên quan