Tại sao CancellingToken tách biệt với CancellingTokenSource?


136

Tôi đang tìm kiếm một lý do tại sao .NET CancellationTokenstruct được giới thiệu ngoài CancellationTokenSourcelớp. Tôi hiểu cách sử dụng API, nhưng cũng muốn hiểu tại sao nó được thiết kế theo cách đó.

Tức là tại sao chúng ta có:

var cts = new CancellationTokenSource();
SomeCancellableOperation(cts.Token);

...
public void SomeCancellableOperation(CancellationToken token) {
    ...
    token.ThrowIfCancellationRequested();
    ...
}

thay vì trực tiếp đi qua CancellationTokenSourcenhư:

var cts = new CancellationTokenSource();
SomeCancellableOperation(cts);

...
public void SomeCancellableOperation(CancellationTokenSource cts) {
    ...
    cts.ThrowIfCancellationRequested();
    ...
}

Đây có phải là một tối ưu hóa hiệu suất dựa trên thực tế là việc kiểm tra trạng thái hủy xảy ra thường xuyên hơn so với việc chuyển mã thông báo xung quanh?

Vì vậy, CancellationTokenSourcecó thể theo dõi và cập nhật CancellationTokens, và đối với mỗi mã thông báo, kiểm tra hủy là truy cập trường địa phương?

Cho rằng một bool dễ bay hơi không có khóa là đủ trong cả hai trường hợp, tôi vẫn không thể hiểu tại sao điều đó sẽ nhanh hơn.

Cảm ơn!

Câu trả lời:


109

Tôi đã tham gia vào việc thiết kế và thực hiện các lớp này.

Câu trả lời ngắn gọn là " tách mối quan tâm ". Hoàn toàn đúng là có nhiều chiến lược thực hiện khác nhau và một số đơn giản hơn ít nhất là về hệ thống loại và học tập ban đầu. Tuy nhiên, CTS và CT được thiết kế để sử dụng trong nhiều tình huống lớn (chẳng hạn như ngăn xếp thư viện sâu, tính toán song song, không đồng bộ, v.v.) và do đó được thiết kế với nhiều trường hợp sử dụng phức tạp. Đây là một thiết kế nhằm khuyến khích các mẫu thành công và không khuyến khích các mẫu chống mà không làm giảm hiệu suất.

Nếu cánh cửa bị bỏ ngỏ cho các API hoạt động sai, thì tính hữu ích của thiết kế hủy có thể nhanh chóng bị xói mòn.

CancellationTokenSource == "kích hoạt hủy", cộng với tạo trình nghe được liên kết

CancellationToken == "người nghe hủy bỏ"


7
Cảm ơn rất nhiều! Các kiến ​​thức bên trong thực sự được đánh giá cao.
Andrey Tarantsov

stackoverflow.com/questions/39077497/ Từ Bạn có biết thời gian chờ mặc định cho dòng đầu vào của StreamSocket không? Khi tôi sử dụng Cancationtoken để hủy thao tác đọc, nó cũng đóng ổ cắm liên quan. Có thể có cách nào để khắc phục vấn đề này?
sam18

@Mike - Chỉ tò mò: tại sao bạn không thể gọi một cái gì đó như throw IfCancnameRequested () trên CTS như bạn có thể trên CT?
rory.ap

Họ có trách nhiệm khác nhau và không quá dài dòng để viết cts.Token.Throw IfCancnameRequested () vì vậy chúng tôi đã không thêm API người nghe trực tiếp trên CTS.
Mike Liddell

@Mike Tại sao Cancationtoken là một cấu trúc và không phải là lớp? Tôi không thấy bất kỳ lợi thế về hiệu suất
Neir0

85

Tôi đã có câu hỏi chính xác và tôi muốn hiểu lý do căn bản đằng sau thiết kế này.

Câu trả lời được chấp nhận có lý do chính xác. Đây là xác nhận từ nhóm đã thiết kế tính năng này (nhấn mạnh của tôi):

Hai loại mới tạo thành nền tảng của khung: A CancellationTokenlà một cấu trúc đại diện cho 'yêu cầu hủy bỏ tiềm năng'. Cấu trúc này được truyền vào các cuộc gọi phương thức như một tham số và phương thức có thể thăm dò trên nó hoặc đăng ký một cuộc gọi lại để được hủy bỏ khi yêu cầu hủy bỏ. A CancellationTokenSourcelà một lớp cung cấp cơ chế để bắt đầu yêu cầu hủy và nó có một thuộc Token tính để nhận mã thông báo liên quan. Sẽ là điều tự nhiên khi kết hợp hai lớp này thành một, nhưng thiết kế này cho phép hai thao tác chính (bắt đầu yêu cầu hủy so với quan sát và phản hồi hủy) được tách biệt rõ ràng. Cụ thể, các phương thức chỉ mất một CancellationTokencó thể quan sát yêu cầu hủy nhưng không thể bắt đầu một yêu cầu.

Liên kết: Khung hủy bỏ .NET 4

Theo tôi, việc CancellationTokenchỉ có thể quan sát trạng thái và không thay đổi nó, là vô cùng quan trọng. Bạn có thể phát mã thông báo như một viên kẹo và không bao giờ lo lắng rằng ai đó, ngoài bạn, sẽ hủy nó. Nó bảo vệ bạn khỏi mã bên thứ ba thù địch. Vâng, cơ hội là rất mong manh, nhưng cá nhân tôi thích sự đảm bảo đó.

Tôi cũng cảm thấy rằng nó làm cho API sạch hơn và tránh được lỗi vô ý và thúc đẩy thiết kế thành phần tốt hơn.

Hãy xem API công khai cho cả hai lớp này.

API hủy bỏ

API CancellingTokenSource

Nếu bạn kết hợp chúng, khi viết LongRastyFunction, tôi sẽ thấy các phương thức giống như nhiều lần quá tải 'Hủy' mà tôi không nên sử dụng. Cá nhân, tôi ghét phải xem phương pháp Vứt bỏ là tốt.

Tôi nghĩ rằng thiết kế lớp hiện tại tuân theo triết lý 'hố thành công', nó hướng dẫn các nhà phát triển tạo ra các thành phần tốt hơn có thể xử lý Taskhủy bỏ và sau đó kết hợp chúng lại với nhau theo nhiều cách để tạo ra các quy trình công việc phức tạp.

Hãy để tôi hỏi bạn một câu hỏi, bạn có tự hỏi mục đích của token.Register là gì không? Nó không có ý nghĩa với tôi. Và sau đó tôi đọc Hủy trong Chủ đề được quản lý và mọi thứ trở nên rõ ràng.

Tôi tin rằng Thiết kế khung hủy trong TPL là đỉnh cao.


2
Nếu bạn tự hỏi làm thế nào CancellationTokenSourcethực sự có thể bắt đầu yêu cầu hủy trên mã thông báo được liên kết của nó (mã thông báo không thể tự thực hiện): CancellingToken có hàm tạo bên trong này: internal CancellationToken(CancellationTokenSource source) { this.m_source = source; }và thuộc tính này: public bool IsCancellationRequested { get { return this.m_source != null && this.m_source.IsCancellationRequested; } }CancellingTokenSource sử dụng hàm tạo bên trong, vì vậy mã thông báo có tham chiếu đến nguồn (m_source)
chviLadislav 28/03/2017

65

Chúng riêng biệt không phải vì lý do kỹ thuật mà là ngữ nghĩa. Nếu bạn nhìn vào việc triển khai CancellationTokentheo ILSpy, bạn sẽ thấy đó chỉ là một trình bao bọc xung quanh CancellationTokenSource(và do đó không khác biệt về hiệu năng so với việc chuyển qua một tham chiếu).

Chúng cung cấp sự phân tách chức năng này để làm cho mọi thứ dễ dự đoán hơn: khi bạn truyền một phương thức a CancellationToken, bạn biết bạn vẫn là người duy nhất có thể hủy bỏ nó. Chắc chắn, phương thức vẫn có thể ném một TaskCancelledException, nhưng CancellationTokenchính nó - và bất kỳ phương thức nào khác tham chiếu cùng một mã thông báo - sẽ vẫn an toàn.


Cảm ơn! Phản ứng chớp mắt của tôi là, về mặt ngữ nghĩa, đó là một cách tiếp cận đáng ngờ, ngang với việc cung cấp IReadOnlyList. Mặc dù nghe có vẻ rất hợp lý, vì vậy, chấp nhận câu trả lời của bạn.
Andrey Tarantsov

1
Khi suy nghĩ thêm, tôi thấy mình nghi ngờ tính hợp lý. Sau đó, sẽ không có ý nghĩa hơn khi cung cấp giao diện ICancnameToken mà CancnameTokenSource sẽ triển khai? Tôi ước ai đó từ nhóm .NET sẽ hòa nhập.
Andrey Tarantsov

Điều đó vẫn có thể dẫn đến một lập trình viên xảo quyệt CancellationTokenSource. Bạn sẽ nghĩ rằng bạn chỉ có thể nói "đừng làm vậy", nhưng mọi người (bao gồm cả tôi!) Thỉnh thoảng vẫn làm những việc này để có được một số chức năng ẩn và điều đó sẽ xảy ra. Đó là lý thuyết hiện tại của tôi, ít nhất.
Cory Nelson

1
Đó không phải là cách bạn thiết kế API trong hầu hết các trường hợp, trừ khi nó liên quan đến bảo mật. Tôi thà đặt cược vào một số lời giải thích khác, như là giữ cho các tùy chọn của họ mở để tối ưu hóa hiệu suất trong tương lai. Tuy nhiên, của bạn là tốt nhất cho đến nay.
Andrey Tarantsov

Này, tôi hy vọng bạn sẽ tha thứ cho tôi khi chỉ định lại câu trả lời được chấp nhận cho người liên quan đến việc thực hiện. Trong khi cả hai bạn đều nói điều tương tự, tôi nghĩ SO được lợi từ câu trả lời dứt khoát đang đứng đầu.
Andrey Tarantsov

10

Đây CancellationTokenlà một cấu trúc để nhiều bản sao có thể tồn tại do truyền nó cho các phương thức.

Các CancellationTokenSourcebộ trạng thái của tất cả các bản sao của một mã thông báo khi gọi Cancelvào nguồn. Xem trang MSDN này

Lý do cho thiết kế có thể chỉ là vấn đề tách biệt mối quan tâm và tốc độ của một cấu trúc.


1
Cảm ơn. Lưu ý rằng một câu trả lời khác nói rằng về mặt kỹ thuật (trong IL), CancellingTokenSource không đặt trạng thái mã thông báo; thay vào đó, các mã thông báo bao bọc một tham chiếu thực tế đến CancellingTokenSource và chỉ cần truy cập nó để kiểm tra hủy bỏ. Nếu bất cứ điều gì, tốc độ chỉ có thể bị mất ở đây.
Andrey Tarantsov

+1. Tôi không hiểu nếu nó là một loại giá trị để mỗi phương thức có giá trị riêng (giá trị riêng). vậy làm thế nào để một phương thức biết nếu hủy bỏ đã được gọi? nó KHÔNG có bất kỳ tham chiếu nào đến TokenSource. tất cả các phương thức có thể thấy là một loại giá trị cục bộ như "5". bạn có thể giải thích ?
Royi Namir

1
@RoyiNamir: Mỗi CancellingToken có tham chiếu riêng "m_source" thuộc loại
CancellingTokenSource

2

Các CancellationTokenSourcelà 'điều' rằng các vấn đề việc hủy bỏ, vì lý do gì. Nó cần một cách để 'gửi' sự hủy bỏ đó đến tất cả những CancellationTokengì nó đã ban hành. Đó là cách, ví dụ, ASP.NET có thể hủy các hoạt động khi yêu cầu bị hủy bỏ. Mỗi yêu cầu có một CancellationTokenSourcechuyển tiếp hủy bỏ tất cả các mã thông báo mà nó đã phát hành.

Điều này tuyệt vời để kiểm tra đơn vị BTW - tạo nguồn mã thông báo hủy của riêng bạn, nhận mã thông báo, gọi Cancelnguồn và chuyển mã thông báo đến mã của bạn để xử lý việc hủy.


Cảm ơn - nhưng cả hai nhiệm vụ (trong đó đang rất liên quan) có thể được trao cho cùng lớp. Không thực sự giải thích tại sao chúng là riêng biệt.
Andrey Tarantsov
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.