Warum ist CancellationToken von CancellationTokenSource getrennt?

136

Ich suche nach einer Begründung, warum .NET CancellationTokenstruct zusätzlich zum CancellationTokenSourceUnterricht eingeführt wurde. Ich verstehe, wie die API verwendet werden soll, möchte aber auch verstehen, warum sie so konzipiert ist.

Dh warum haben wir:

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

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

anstatt direkt herumzugeben CancellationTokenSourcewie:

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

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

Handelt es sich um eine Leistungsoptimierung, die auf der Tatsache basiert, dass Abbruchstatusprüfungen häufiger stattfinden als das Weitergeben des Tokens?

Damit CancellationTokenSourcekönnen Sie verfolgen und aktualisieren CancellationTokens, und für jedes Token ist die Stornierungsprüfung ein lokaler Feldzugriff?

Angesichts der Tatsache, dass in beiden Fällen ein flüchtiger Bool ohne Verriegelung ausreicht, kann ich immer noch nicht erkennen, warum dies schneller wäre.

Vielen Dank!

Andrey Tarantsov
quelle

Antworten:

109

Ich war an der Gestaltung und Implementierung dieser Klassen beteiligt.

Die kurze Antwort lautet " Trennung von Bedenken ". Es ist durchaus richtig, dass es verschiedene Implementierungsstrategien gibt und dass einige zumindest in Bezug auf das Typensystem und das anfängliche Lernen einfacher sind. CTS und CT sind jedoch für die Verwendung in einer Vielzahl von Szenarien vorgesehen (z. B. Deep Library Stacks, parallele Berechnung, Async usw.) und wurden daher für viele komplexe Anwendungsfälle entwickelt. Es ist ein Design, das erfolgreiche Muster fördern und Anti-Muster entmutigen soll, ohne die Leistung zu beeinträchtigen.

Wenn die Tür für sich schlecht verhaltende APIs offen gelassen würde, könnte die Nützlichkeit des Stornierungsdesigns schnell beeinträchtigt werden.

CancellationTokenSource == "Abbruchauslöser" plus generiert verknüpfte Listener

CancellationToken == "Stornierungslistener"

Mike Liddell
quelle
7
Vielen Dank! Das Insiderwissen wird sehr geschätzt.
Andrey Tarantsov
stackoverflow.com/questions/39077497/… Haben Sie eine Idee, wie das Standardzeitlimit für den Eingabestream des StreamSocket aussehen soll? Wenn ich Cancelationtoken verwende, um den Lesevorgang abzubrechen, wird auch der betreffende Socket geschlossen. Kann es eine Möglichkeit geben, dieses Problem zu lösen?
Sam18
@Mike - Nur neugierig: Wie kommt es, dass Sie so etwas wie ThrowIfCancellationRequested () auf einem CTS nicht wie auf einem CT aufrufen können?
rory.ap
Sie haben unterschiedliche Verantwortlichkeiten und es ist nicht zu ausführlich, cts.Token.ThrowIfCancellationRequested () zu schreiben, sodass wir die Listener-API nicht direkt in CTS hinzugefügt haben.
Mike Liddell
@Mike Warum ist CancellationToken eine Struktur und keine Klasse? Ich sehe keine Leistungsvorteile
Neir0
85

Ich hatte die genaue Frage und wollte die Gründe für dieses Design verstehen.

Die akzeptierte Antwort brachte die Begründung genau richtig. Hier ist die Bestätigung des Teams, das diese Funktion entwickelt hat (Schwerpunkt Mine):

Zwei neue Typen bilden die Grundlage des Frameworks: A CancellationTokenist eine Struktur, die einen „potenziellen Antrag auf Stornierung“ darstellt. Diese Struktur wird als Parameter an Methodenaufrufe übergeben, und die Methode kann sie abfragen oder einen Rückruf registrieren, der ausgelöst wird, wenn eine Stornierung angefordert wird. A CancellationTokenSourceist eine Klasse, die den Mechanismus zum Initiieren einer Stornierungsanforderung bereitstellt und über eine Token Eigenschaft zum Abrufen eines zugeordneten Tokens verfügt. Es wäre natürlich gewesen, diese beiden Klassen zu einer zu kombinieren, aber dieses Design ermöglicht es, die beiden Schlüsseloperationen (Initiieren einer Stornierungsanforderung vs. Beobachten und Reagieren auf Stornierung) sauber zu trennen. Insbesondere Methoden, die nur a annehmen, CancellationTokenkönnen eine Stornierungsanforderung beobachten, aber keine initiieren.

Link: .NET 4 Cancellation Framework

Meiner Meinung nach ist die Tatsache, dass CancellationTokenman den Zustand nur beobachten und nicht ändern kann, äußerst kritisch. Sie können den Token wie eine Süßigkeit verteilen und müssen sich niemals Sorgen machen, dass jemand anderes als Sie ihn storniert. Es schützt Sie vor feindlichem Code von Drittanbietern. Ja, die Chancen stehen schlecht, aber ich persönlich mag diese Garantie.

Ich habe auch das Gefühl, dass es die API sauberer macht, versehentliche Fehler vermeidet und ein besseres Komponentendesign fördert.

Schauen wir uns die öffentliche API für beide Klassen an.

CancellationToken API

CancellationTokenSource-API

Wenn Sie sie kombinieren, werden beim Schreiben von LongRunningFunction Methoden wie die mehrfachen Überladungen von 'Abbrechen' angezeigt, die ich nicht verwenden sollte. Persönlich hasse ich es, auch die Dispose-Methode zu sehen.

Ich denke, das aktuelle Klassendesign folgt der "Pit of Success" -Philosophie. Es führt Entwickler dazu, bessere Komponenten zu erstellen, die mit TaskStornierungen umgehen können, und sie dann auf vielfältige Weise zusammenzustellen, um komplizierte Workflows zu erstellen.

Lassen Sie mich Ihnen eine Frage stellen. Haben Sie sich gefragt, was der Zweck von Token ist? Registrieren? Es ergab für mich keinen Sinn. Und dann habe ich gelesen Stornierung in verwalteten Threads und alles wurde kristallklar.

Ich glaube, dass das Cancellation Framework Design in TPL absolut erstklassig ist.

SolutionYogi
quelle
2
Wenn Sie sich fragen, wie der CancellationTokenSourcedie Abbruchanforderung für das zugehörige Token tatsächlich initiieren kann (das Token kann dies nicht selbst tun): Das CancellationToken verfügt über diesen internen Konstruktor: internal CancellationToken(CancellationTokenSource source) { this.m_source = source; }und diese Eigenschaft: public bool IsCancellationRequested { get { return this.m_source != null && this.m_source.IsCancellationRequested; } }Die CancellationTokenSource verwendet den internen Konstruktor, sodass das Token auf das verweist Quelle (m_source)
chviLadislav
65

Sie sind nicht aus technischen, sondern aus semantischen Gründen getrennt. Wenn Sie sich die Implementierung von CancellationTokenunter ILSpy ansehen , werden Sie feststellen, dass es sich lediglich um einen Wrapper handeltCancellationTokenSource (und daher in Bezug auf die Leistung nicht anders ist als das Weitergeben einer Referenz).

Sie bieten diese Trennung von Funktionen, um die Vorhersehbarkeit zu verbessern: Wenn Sie eine Methode a übergeben CancellationToken, wissen Sie, dass Sie immer noch die einzige sind, die sie abbrechen kann. Sicher, die Methode könnte immer noch ein werfen TaskCancelledException, aber das CancellationTokenselbst - und alle anderen Methoden, die auf dasselbe Token verweisen - würden sicher bleiben.

Cory Nelson
quelle
Vielen Dank! Meine Blinkreaktion ist, dass es semantisch gesehen ein fragwürdiger Ansatz ist, der der Bereitstellung von IReadOnlyList gleichkommt. Es klingt jedoch sehr plausibel, also akzeptieren Sie Ihre Antwort.
Andrey Tarantsov
1
Beim weiteren Nachdenken stelle ich die Plausibilität in Frage. Wäre es dann nicht sinnvoller, eine ICancellationToken-Schnittstelle bereitzustellen, die CancellationTokenSource implementieren würde? Ich wünschte, jemand aus dem .NET-Team würde sich
einschalten
Das könnte immer noch zu einem schlauen Programmierer führen CancellationTokenSource. Sie würden denken, Sie könnten einfach sagen "Tu das nicht", aber Leute (einschließlich ich!) Tun diese Dinge gelegentlich trotzdem, um an versteckte Funktionen zu gelangen, und es würde passieren. Das ist zumindest meine aktuelle Theorie.
Cory Nelson
1
So entwerfen Sie APIs in den meisten Fällen nicht, es sei denn, es handelt sich um sicherheitsrelevante APIs. Ich würde lieber auf eine andere Erklärung wetten, wie „die Optionen für zukünftige Leistungsoptimierungen offen zu halten“. Trotzdem ist deine bisher die beste.
Andrey Tarantsov
Hey, ich hoffe, Sie entschuldigen mich dafür, dass ich die akzeptierte Antwort der Person zugewiesen habe, die an der Implementierung beteiligt ist. Während Sie beide dasselbe sagen, denke ich, dass SO von der endgültigen Antwort profitiert, die oben steht.
Andrey Tarantsov
10

Das CancellationTokenist eine Struktur, bei der so viele Kopien existieren könnten, weil sie an Methoden weitergegeben werden.

Das CancellationTokenSourcelegt den Status ALLER Kopien eines Tokens fest, wenn Canceldie Quelle aufgerufen wird. Siehe diese MSDN-Seite

Der Grund für das Design könnte nur eine Frage der Trennung von Bedenken und der Geschwindigkeit einer Struktur sein.

Erno
quelle
1
Vielen Dank. Beachten Sie, dass eine andere Antwort besagt, dass CancellationTokenSource technisch (in IL) den Status von Token nicht festlegt. Stattdessen umschließen die Token einen tatsächlichen Verweis auf CancellationTokenSource und greifen einfach darauf zu, um nach einer Stornierung zu suchen. Wenn überhaupt, kann die Geschwindigkeit nur hier verloren gehen.
Andrey Tarantsov
+1. Ich verstehe nicht. Wenn es sich um einen Werttyp handelt, hat jede Methode ihren eigenen Wert (separater Wert). Woher weiß eine Methode, ob ein Abbruch aufgerufen wurde? Es enthält KEINEN Verweis auf die TokenSource. Die Methode kann nur einen lokalen Werttyp wie "5" sehen. können Sie erklären ?
Royi Namir
1
@ RoyiNamir: Jedes CancellationToken hat private Referenz "m_source" vom Typ CancellationTokenSource
Andreyul
2

Das CancellationTokenSourceist das "Ding", das die Stornierung aus irgendeinem Grund ausstellt. Es braucht eine Möglichkeit, diese Stornierung an alle von CancellationTokenihr ausgestellten zu versenden . So kann beispielsweise ASP.NET Vorgänge abbrechen, wenn eine Anforderung abgebrochen wird. Jede Anfrage hat eine CancellationTokenSource, die die Stornierung an alle von ihr ausgegebenen Token weiterleitet.

Dies ist ideal für Unit-Tests. Übrigens: Erstellen Sie Ihre eigene Quelle für Stornierungstoken, holen Sie sich ein Token, rufen Sie Canceldie Quelle auf und übergeben Sie das Token an Ihren Code, der die Stornierung verarbeiten muss.

n8wrl
quelle
Danke - aber beide Aufgaben (die sind sehr verwandt) könnte zu der gleichen Klasse gegeben werden. Erklärt nicht wirklich, warum sie getrennt sind.
Andrey Tarantsov