Dlaczego CancellationToken jest oddzielony od CancellationTokenSource?

139

Szukam uzasadnienia, dlaczego CancellationTokenstruktura .NET została wprowadzona jako dodatek do CancellationTokenSourceklasy. Rozumiem, jak należy używać interfejsu API, ale chcę również zrozumieć, dlaczego jest tak zaprojektowany.

To znaczy, dlaczego mamy:

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

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

zamiast bezpośredniego przekazywania, CancellationTokenSourcejak:

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

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

Czy jest to optymalizacja wydajności oparta na fakcie, że sprawdzanie stanu anulowania odbywa się częściej niż przekazywanie tokenu?

Więc to CancellationTokenSourcemoże śledzić i aktualizować CancellationTokens, a dla każdego tokena kontrola anulowania jest lokalnym dostępem do pola?

Biorąc pod uwagę, że zmienny bool bez blokowania jest wystarczający w obu przypadkach, nadal nie rozumiem, dlaczego miałoby to być szybsze.

Dzięki!

Andrey Tarantsov
źródło

Odpowiedzi:

113

Brałem udział w projektowaniu i realizacji tych zajęć.

Krótka odpowiedź brzmi: oddzielenie obaw ”. Prawdą jest, że istnieją różne strategie wdrażania, a niektóre są prostsze, przynajmniej jeśli chodzi o system typów i początkową naukę. Jednak CTS i CT są przeznaczone do użytku w wielu scenariuszach (takich jak głębokie stosy bibliotek, obliczenia równoległe, asynchronizacja itp.), A zatem zostały zaprojektowane z myślą o wielu złożonych przypadkach użycia. Jest to projekt mający na celu zachęcanie do udanych wzorców i zniechęcanie do anty-wzorców bez poświęcania wydajności.

Gdyby drzwi pozostały otwarte dla niewłaściwie działających interfejsów API, użyteczność projektu anulowania mogłaby szybko ulec zmniejszeniu.

CancellationTokenSource == "wyzwalacz anulowania" oraz generuje połączone słuchacze

CancellationToken == "odbiornik anulowania"

Mike Liddell
źródło
7
Wielkie dzięki! Ceniona jest wiedza wewnętrzna.
Andrey Tarantsov
stackoverflow.com/questions/39077497/… Czy masz pomysł, jaki powinien być domyślny limit czasu dla strumienia wejściowego StreamSocket? Kiedy używam cancellationtoken do anulowania operacji odczytu, to również zamyka dane gniazdo. Czy istnieje sposób na rozwiązanie tego problemu?
sam18
@Mike - Po prostu ciekawy: dlaczego nie możesz wywołać czegoś takiego jak ThrowIfCancellationRequested () na CTS, tak jak możesz na CT?
rory.ap
Mają różne obowiązki i nie jest zbyt rozwlekłe, aby napisać cts.Token.ThrowIfCancellationRequested (), więc nie dodaliśmy API nasłuchującego bezpośrednio w CTS.
Mike Liddell
@Mike Dlaczego cancellationtoken jest strukturą, a nie klasą? Nie widzę żadnych korzyści w zakresie wydajności
Neir0
87

Miałem dokładne pytanie i chciałem zrozumieć uzasadnienie tego projektu.

Zaakceptowana odpowiedź miała dokładnie prawidłowe uzasadnienie. Oto potwierdzenie od zespołu, który zaprojektował tę funkcję (moje podkreślenie):

Podstawę CancellationTokenstruktury stanowią dwa nowe typy: A to struktura reprezentująca „potencjalny wniosek o anulowanie”. Ta struktura jest przekazywana do wywołań metod jako parametr, a metoda może sondować ją lub rejestrować wywołanie zwrotne, które ma zostać uruchomione, gdy zażąda się anulowania. A CancellationTokenSourceto klasa, która zapewnia mechanizm inicjowania żądania anulowania i ma Token właściwość do uzyskiwania skojarzonego tokenu. Byłoby naturalne połączenie tych dwóch klas w jedną, ale ten projekt pozwala na wyraźne rozdzielenie dwóch kluczowych operacji (zainicjowanie żądania anulowania w porównaniu z obserwacją i odpowiedzią na anulowanie). W szczególności metody, które przyjmują tylko a, CancellationTokenmogą obserwować żądanie anulowania, ale nie mogą go zainicjować.

Link: .NET 4 Cancellation Framework

Moim zdaniem to, że CancellationTokenmożna tylko obserwować stan i go nie zmieniać, jest niezwykle krytyczne. Możesz rozdać token jak cukierek i nigdy nie martwić się, że ktoś inny niż ty go anuluje. Chroni Cię przed wrogim kodem stron trzecich. Tak, szanse są niewielkie, ale osobiście podoba mi się ta gwarancja.

Uważam również, że sprawia, że ​​API jest czystsze, pozwala uniknąć przypadkowych pomyłek i promuje lepsze projektowanie komponentów.

Spójrzmy na publiczne API dla obu tych klas.

CancellationToken API

CancellationTokenSource API

Jeśli miałbyś je połączyć, pisząc LongRunningFunction, zobaczę metody takie jak te wielokrotne przeciążenia „Anuluj”, których nie powinienem używać. Osobiście nienawidzę również widzieć metody Dispose.

Myślę, że obecny projekt klasy jest zgodny z filozofią „dołu sukcesu”, prowadzi programistów do tworzenia lepszych komponentów, które radzą sobie z Taskanulowaniem, a następnie łączy je na różne sposoby, tworząc skomplikowane przepływy pracy.

Pozwól, że zadam Ci pytanie, czy zastanawiałeś się, jaki jest cel tokena. To nie miało dla mnie sensu. A potem przeczytałem Anulowanie w zarządzanych wątkach i wszystko stało się krystalicznie jasne.

Uważam, że projekt ramy anulowania w TPL jest absolutnie na najwyższym poziomie.

Rozwiązanie Yogi
źródło
2
Jeśli zastanawiasz się, w jaki sposób CancellationTokenSourcemoże faktycznie zainicjować żądanie anulowania na swoim powiązanym tokenie (token nie może tego zrobić sam): CancellationToken ma ten wewnętrzny konstruktor: internal CancellationToken(CancellationTokenSource source) { this.m_source = source; }i ta właściwość: public bool IsCancellationRequested { get { return this.m_source != null && this.m_source.IsCancellationRequested; } }CancellationTokenSource używa wewnętrznego konstruktora, więc token ma odwołanie do źródło (m_source)
chviLadislav
65

Są oddzielne nie ze względów technicznych, ale semantycznych. Jeśli spojrzysz na implementację CancellationTokenpod ILSpy, zobaczysz, że jest to tylko opakowanie CancellationTokenSource(a zatem nie ma innej wydajności pod względem wydajności niż przekazywanie referencji).

Zapewniają to oddzielenie funkcji, aby uczynić rzeczy bardziej przewidywalnymi: kiedy przekazujesz metodę a CancellationToken, wiesz, że nadal jesteś jedyną osobą, która może ją anulować. Jasne, metoda może nadal rzucać TaskCancelledException, ale CancellationTokensama - i wszelkie inne metody odwołujące się do tego samego tokenu - pozostaną bezpieczne.

Cory Nelson
źródło
Dzięki! Moja reakcja mrugnięcia jest taka, że ​​semantycznie jest to wątpliwe podejście, na równi z dostarczeniem IReadOnlyList. Brzmi to jednak bardzo wiarygodnie, więc zaakceptuj swoją odpowiedź.
Andrey Tarantsov
1
Po dalszym myśleniu stwierdzam, że kwestionuję wiarygodność. Czy nie byłoby zatem bardziej sensowne udostępnienie interfejsu ICancellationToken, który zaimplementowałby CancellationTokenSource? Chciałbym, żeby ktoś z zespołu .NET się
włączył
Może to nadal skutkować rzutowaniem zręcznego programisty na CancellationTokenSource. Można by pomyśleć, że możesz po prostu powiedzieć „nie rób tego”, ale ludzie (w tym ja!) I tak czasami robią te rzeczy, aby dostać się do jakiejś ukrytej funkcjonalności, i tak się stanie. Przynajmniej taka jest moja obecna teoria.
Cory Nelson
1
W większości przypadków nie projektuje się interfejsów API, chyba że jest to związane z bezpieczeństwem. Wolałbym raczej postawić na inne wyjaśnienie, na przykład „pozostawienie ich otwartych opcji w celu optymalizacji wydajności w przyszłości”. Ale nadal twój jest najlepszy do tej pory.
Andrey Tarantsov
Hej, mam nadzieję, że wybaczycie mi ponowne przypisanie zaakceptowanej odpowiedzi osobie zaangażowanej we wdrożenie. Chociaż oboje mówicie to samo, myślę, że SO korzysta z ostatecznej odpowiedzi na wierzchu.
Andrey Tarantsov
10

CancellationTokenJest struct tak wiele egzemplarzy może istnieć ze względu na przepuszczenie go wraz z metodami.

Te CancellationTokenSourcezestawy stan wszystkich kopii tokena podczas wywoływania Cancelod źródła. Zobacz tę stronę MSDN

Przyczyną projektu może być tylko kwestia oddzielenia problemów i szybkości konstrukcji.

Eee nie
źródło
1
Dzięki. Zauważ, że inna odpowiedź mówi, że technicznie (w IL) CancellationTokenSource nie ustawia stanu tokenów; zamiast tego tokeny zawijają rzeczywiste odwołanie do CancellationTokenSource i po prostu uzyskują do niego dostęp w celu sprawdzenia anulowania. Jeśli już, prędkość można stracić tylko tutaj.
Andrey Tarantsov
+1. Nie rozumiem. jeśli jest to typ wartości, więc każda metoda ma swoją własną wartość (oddzielną wartość). więc skąd metoda wie, czy wywołano anulowanie? NIE ma żadnego odniesienia do TokenSource. jedyną metodą, którą widzi, jest lokalny typ wartości, np. „5”. możesz wytłumaczyć ?
Royi Namir
1
@RoyiNamir: Każdy CancellationToken ma prywatne odniesienie „m_source” typu CancellationTokenSource
Andreyul,
2

To CancellationTokenSourcejest „rzecz”, która powoduje anulowanie rezerwacji z dowolnego powodu. Potrzebuje sposobu na „wysłanie” tego anulowania do wszystkich CancellationTokenwydanych. W ten sposób, na przykład, ASP.NET może anulować operacje, gdy żądanie zostanie przerwane. Każde żądanie ma przesłanie CancellationTokenSourceanulowania do wszystkich wydanych tokenów.

To świetne rozwiązanie do testowania jednostkowego BTW - utwórz własne źródło tokenu anulowania, uzyskaj token, wywołaj Cancelźródło i przekaż token do kodu, który ma obsługiwać anulowanie.

n8wrl
źródło
Dzięki - ale obie obowiązki (które bardzo powiązane) można powierzyć tej samej klasie. Tak naprawdę nie wyjaśnia, dlaczego są oddzielne.
Andrey Tarantsov