Hủy mã thông báo trong Trình xây dựng tác vụ: tại sao?


223

Các hàm System.Threading.Tasks.Tasktạo nhất định lấy CancellationTokentham số:

CancellationTokenSource source = new CancellationTokenSource();
Task t = new Task (/* method */, source.Token);

Điều gây trở ngại cho tôi về điều này là không có cách nào từ bên trong cơ thể phương thức để thực sự nhận được mã thông báo được truyền vào (ví dụ, không có gì giống như Task.CurrentTask.CancellationToken). Mã thông báo phải được cung cấp thông qua một số cơ chế khác, chẳng hạn như đối tượng trạng thái hoặc bị bắt trong lambda.

Vì vậy, mục đích nào cung cấp mã thông báo hủy trong hàm tạo phục vụ?

Câu trả lời:


254

Truyền một CancellationTokenvào Taskconstructor liên kết nó với nhiệm vụ.

Trích dẫn câu trả lời của Stephen Toub từ MSDN :

Điều này có hai lợi ích chính:

  1. Nếu mã thông báo có yêu cầu hủy trước khi Taskbắt đầu thực thi, thì mã Tasksẽ không được thực thi. Thay vì chuyển sang Running, nó sẽ ngay lập tức chuyển sang Canceled. Điều này tránh được các chi phí khi chạy tác vụ nếu nó sẽ bị hủy trong khi vẫn chạy.
  2. Nếu phần thân của tác vụ cũng đang theo dõi mã thông báo hủy và ném mã thông báo OperationCanceledExceptioncó chứa (đó là cái gì ThrowIfCancellationRequested), thì khi tác vụ thấy điều đó OperationCanceledException, nó sẽ kiểm tra xem OperationCanceledExceptionmã thông báo có khớp với mã thông báo của Nhiệm vụ không. Nếu có, ngoại lệ đó được xem như là một sự thừa nhận hủy bỏ hợp tác và Taskchuyển đổi sang Canceled nhà nước (chứ không phải là Faultednhà nước).

2
TPL là rất tốt nghĩ ra.
Đại tá Panic

1
Tôi giả sử lợi ích 1 áp dụng tương tự như chuyển mã thông báo hủy cho Parallel.ForhoặcParallel.ForEach
Đại tá Panic

27

Hàm tạo sử dụng mã thông báo để xử lý hủy trong nội bộ. Nếu mã của bạn muốn truy cập vào mã thông báo, bạn có trách nhiệm chuyển nó cho chính mình. Tôi đặc biệt khuyên bạn nên đọc Lập trình song song với sách Microsoft .NET tại CodePlex .

Ví dụ sử dụng CTS từ cuốn sách:

CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

Task myTask = Task.Factory.StartNew(() =>
{
    for (...)
    {
        token.ThrowIfCancellationRequested();

        // Body of for loop.
    }
}, token);

// ... elsewhere ...
cts.Cancel();

3
và điều gì xảy ra nếu bạn không chuyển mã thông báo làm tham số? Hình như hành vi sẽ giống nhau, không có mục đích.
sergtk

2
@sergdev: bạn chuyển mã thông báo để đăng ký nó với tác vụ và lịch trình. Không vượt qua nó và sử dụng nó sẽ là hành vi không xác định.
user7116

3
@sergdev: sau khi thử nghiệm: myTask.Is Hủyed và myTask.Status không giống nhau khi bạn không chuyển mã thông báo làm tham số. Tình trạng sẽ không thành công thay vì hủy bỏ. Tuy nhiên, ngoại lệ là như nhau: đó là một hoạt động Hủy bỏ ngoại lệ trong cả hai trường hợp.
Olivier de Rivoyre 16/07/2015

2
Nếu tôi không gọi token.ThrowIfCancellationRequested();thì sao? Trong thử nghiệm của tôi, hành vi là như nhau. Có ý kiến ​​gì không?
máy móc

1
@CobaltBlue: không when cts.Cancel() is called the Task is going to get canceled and end, no matter what you do. Nếu Nhiệm vụ bị hủy trước khi nó bắt đầu, nó sẽ bị hủy . Nếu phần thân của Tác vụ đơn giản không bao giờ kiểm tra bất kỳ mã thông báo nào, nó sẽ chạy đến khi hoàn thành, dẫn đến trạng thái RanToCompletion . Nếu cơ thể ném một cái OperationCancelledException, ví dụ như ThrowIfCancellationRequested, thì Nhiệm vụ sẽ kiểm tra xem CancellingToken của Exception có giống với cái được liên kết với Nhiệm vụ hay không. Nếu có, nhiệm vụ sẽ bị hủy . Nếu không, nó đứt gãy .
Wolfzoon

7

Hủy bỏ không phải là một trường hợp đơn giản như nhiều người nghĩ. Một số sự tinh tế được giải thích trong bài đăng trên blog này trên msdn:

Ví dụ:

Trong một số trường hợp nhất định trong Tiện ích mở rộng song song và trong các hệ thống khác, cần phải đánh thức một phương thức bị chặn vì những lý do không phải do người dùng hủy bỏ rõ ràng. Ví dụ: nếu một luồng bị chặn blockingCollection.Take()do bộ sưu tập bị trống và một luồng khác sau đó gọi blockingCollection.CompleteAdding(), thì cuộc gọi đầu tiên sẽ thức dậy và ném một InvalidOperationExceptionđại diện cho việc sử dụng không chính xác.

Hủy bỏ trong phần mở rộng song song


4

Dưới đây là một ví dụ minh họa hai điểm trong câu trả lời của Max Galkin :

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Start canceled task, don't pass token to constructor");
        Console.WriteLine("*********************************************************************");
        StartCanceledTaskTest(false);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Start canceled task, pass token to constructor");
        Console.WriteLine("*********************************************************************");
        StartCanceledTaskTest(true);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Throw if cancellation requested, don't pass token to constructor");
        Console.WriteLine("*********************************************************************");
        ThrowIfCancellationRequestedTest(false);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Throw if cancellation requested, pass token to constructor");
        Console.WriteLine("*********************************************************************");
        ThrowIfCancellationRequestedTest(true);
        Console.WriteLine();

        Console.WriteLine();
        Console.WriteLine("Test Done!!!");
        Console.ReadKey();
    }

    static void StartCanceledTaskTest(bool passTokenToConstructor)
    {
        Console.WriteLine("Creating task");
        CancellationTokenSource tokenSource = new CancellationTokenSource();
        Task task = null;
        if (passTokenToConstructor)
        {
            task = new Task(() => TaskWork(tokenSource.Token, false), tokenSource.Token);

        }
        else
        {
            task = new Task(() => TaskWork(tokenSource.Token, false));
        }

        Console.WriteLine("Canceling task");
        tokenSource.Cancel();

        try
        {
            Console.WriteLine("Starting task");
            task.Start();
            task.Wait();
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception: {0}", ex.Message);
            if (ex.InnerException != null)
            {
                Console.WriteLine("InnerException: {0}", ex.InnerException.Message);
            }
        }

        Console.WriteLine("Task.Status: {0}", task.Status);
    }

    static void ThrowIfCancellationRequestedTest(bool passTokenToConstructor)
    {
        Console.WriteLine("Creating task");
        CancellationTokenSource tokenSource = new CancellationTokenSource();
        Task task = null;
        if (passTokenToConstructor)
        {
            task = new Task(() => TaskWork(tokenSource.Token, true), tokenSource.Token);

        }
        else
        {
            task = new Task(() => TaskWork(tokenSource.Token, true));
        }

        try
        {
            Console.WriteLine("Starting task");
            task.Start();
            Thread.Sleep(100);

            Console.WriteLine("Canceling task");
            tokenSource.Cancel();
            task.Wait();
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception: {0}", ex.Message);
            if (ex.InnerException != null)
            {
                Console.WriteLine("InnerException: {0}", ex.InnerException.Message);
            }
        }

        Console.WriteLine("Task.Status: {0}", task.Status);
    }

    static void TaskWork(CancellationToken token, bool throwException)
    {
        int loopCount = 0;

        while (true)
        {
            loopCount++;
            Console.WriteLine("Task: loop count {0}", loopCount);

            token.WaitHandle.WaitOne(50);
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("Task: cancellation requested");
                if (throwException)
                {
                    token.ThrowIfCancellationRequested();
                }

                break;
            }
        }
    }
}

Đầu ra:

*********************************************************************
* Start canceled task, don't pass token to constructor
*********************************************************************
Creating task
Canceling task
Starting task
Task: loop count 1
Task: cancellation requested
Task.Status: RanToCompletion

*********************************************************************
* Start canceled task, pass token to constructor
*********************************************************************
Creating task
Canceling task
Starting task
Exception: Start may not be called on a task that has completed.
Task.Status: Canceled

*********************************************************************
* Throw if cancellation requested, don't pass token to constructor
*********************************************************************
Creating task
Starting task
Task: loop count 1
Task: loop count 2
Canceling task
Task: cancellation requested
Exception: One or more errors occurred.
InnerException: The operation was canceled.
Task.Status: Faulted

*********************************************************************
* Throw if cancellation requested, pass token to constructor
*********************************************************************
Creating task
Starting task
Task: loop count 1
Task: loop count 2
Canceling task
Task: cancellation requested
Exception: One or more errors occurred.
InnerException: A task was canceled.
Task.Status: Canceled


Test Done!!!
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.