Por que CancellationToken é separado de CancellationTokenSource?

137

Eu estou procurando uma justificativa do motivo pelo qual a CancellationTokenestrutura do .NET foi introduzida além da CancellationTokenSourceclasse. Entendo como a API deve ser usada, mas também quero entender por que ela foi projetada dessa maneira.

Ou seja, por que temos:

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

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

em vez de passar diretamente CancellationTokenSourcecomo:

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

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

Isso é uma otimização de desempenho com base no fato de que as verificações de estado de cancelamento ocorrem com mais frequência do que a transmissão do token?

Para que você CancellationTokenSourcepossa acompanhar e atualizar CancellationTokens, e para cada token a verificação de cancelamento é um acesso de campo local?

Dado que um bool volátil sem travamento é suficiente nos dois casos, ainda não consigo entender por que isso seria mais rápido.

Obrigado!

Andrey Tarantsov
fonte

Respostas:

110

Eu estava envolvido no design e implementação dessas classes.

A resposta curta é " separação de preocupações ". É bem verdade que existem várias estratégias de implementação e que algumas são mais simples pelo menos no que diz respeito ao sistema de tipos e ao aprendizado inicial. No entanto, o CTS e o CT destinam-se ao uso em muitos cenários (como pilhas profundas de bibliotecas, computação paralela, assíncrona etc.) e, portanto, foram projetados com muitos casos de uso complexos em mente. É um design destinado a incentivar padrões bem-sucedidos e desencorajar anti-padrões sem sacrificar o desempenho.

Se a porta foi deixada aberta por APIs que se comportam mal, a utilidade do design de cancelamento pode ser rapidamente corroída.

CancellationTokenSource == "gatilho de cancelamento", além de gerar ouvintes vinculados

CancellationToken == "ouvinte de cancelamento"

Mike Liddell
fonte
7
Muito obrigado! O conhecimento interno é realmente apreciado.
Andrey Tarantsov
stackoverflow.com/questions/39077497/… Você tem idéia de qual deve ser o tempo limite padrão para o fluxo de entrada do StreamSocket? Quando eu uso cancellationtoken para cancelar a operação de leitura, ele também fecha o soquete em questão. Pode haver alguma maneira de superar esse problema?
precisa saber é
@ Mike - Apenas curioso: como é que você não pode chamar algo como ThrowIfCancellationRequested () em um CTS como você pode em um CT?
Rog.ap
Eles têm responsabilidades diferentes e não é muito detalhado escrever cts.Token.ThrowIfCancellationRequested (), portanto não adicionamos a API do ouvinte diretamente no CTS.
Mike Liddell
@ Mike Por que cancellationtoken é uma estrutura e não uma classe? Eu não vejo nenhuma vantagem performarmance
Neir0
85

Eu tinha a pergunta exata e queria entender a lógica por trás desse design.

A resposta aceita acertou na lógica. Aqui está a confirmação da equipe que criou esse recurso (ênfase minha):

Dois novos tipos formam a base da estrutura: A CancellationTokené uma estrutura que representa uma 'solicitação potencial de cancelamento'. Essa estrutura é passada para as chamadas de método como um parâmetro e o método pode pesquisá-lo ou registrar um retorno de chamada para ser acionado quando o cancelamento for solicitado. A CancellationTokenSourceé uma classe que fornece o mecanismo para iniciar uma solicitação de cancelamento e possui uma Token propriedade para obter um token associado. Teria sido natural combinar essas duas classes em uma, mas esse design permite que as duas operações principais (iniciar uma solicitação de cancelamento versus observar e responder ao cancelamento) sejam separadas adequadamente. Em particular, os métodos que usam apenas a CancellationTokenpodem observar uma solicitação de cancelamento, mas não podem iniciar uma.

Link: Estrutura de cancelamento do .NET 4

Na minha opinião, o fato de CancellationTokenapenas observar o estado e não alterá-lo é extremamente crítico. Você pode distribuir o token como um doce e nunca se preocupar que alguém, além de você, o cancele. Ele protege você contra código hostil de terceiros. Sim, as chances são pequenas, mas eu pessoalmente gosto dessa garantia.

Eu também sinto que isso torna a API mais limpa, evita erros acidentais e promove um melhor design de componentes.

Vamos dar uma olhada na API pública para essas duas classes.

API CancellationToken

API CancellationTokenSource

Se você combiná-los, ao escrever LongRunningFunction, verei métodos como as várias sobrecargas de 'Cancel' que não devo usar. Pessoalmente, eu odeio ver o método Dispose também.

Eu acho que o design da classe atual segue a filosofia do 'poço do sucesso', orienta os desenvolvedores a criar melhores componentes que podem lidar com Task cancelamento e depois instrumentá-los juntos de várias maneiras para criar fluxos de trabalho complicados.

Deixe-me fazer uma pergunta, você já se perguntou qual é o objetivo do token. Não fazia sentido para mim. E então eu li Cancelamento em Threads Gerenciados e tudo ficou claro.

Acredito que o Projeto de Estrutura de Cancelamento no TPL é absolutamente de primeira.

SolutionYogi
fonte
2
Se você se perguntar como o CancellationTokenSourcepode realmente iniciar a solicitação de cancelamento no token associado (o token não pode fazê-lo por si só): O CancellationToken possui este construtor interno: internal CancellationToken(CancellationTokenSource source) { this.m_source = source; }e esta propriedade: public bool IsCancellationRequested { get { return this.m_source != null && this.m_source.IsCancellationRequested; } }O CancellationTokenSource usa o construtor interno, portanto, o token faz referência ao fonte (m_source)
chviLadislav
65

Eles são separados não por razões técnicas, mas semânticas. Se você observar a implementação do CancellationTokenILSpy, verá que é apenas um invólucroCancellationTokenSource (e, portanto, não é diferente em termos de desempenho do que passar uma referência).

Eles fornecem essa separação de funcionalidade para tornar as coisas mais previsíveis: quando você passa um método a CancellationToken, sabe que ainda é o único que pode cancelá-lo. Certamente, o método ainda pode gerar um TaskCancelledException, mas o CancellationTokenpróprio - e quaisquer outros métodos que façam referência ao mesmo token - permanecerão seguros.

Cory Nelson
fonte
Obrigado! Minha reação de piscar é que, semântica, é uma abordagem questionável, a par do fornecimento de IReadOnlyList. Parece muito plausível, portanto, aceitando sua resposta.
Andrey Tarantsov
1
Pensando melhor, me pego questionando a plausibilidade. Não faria mais sentido, então, fornecer a interface ICancellationToken que CancellationTokenSource implementaria? Eu gostaria que alguém da equipe do .NET
gritasse
Isso ainda pode resultar em um programador astuto para CancellationTokenSource. Você pensaria que poderia simplesmente dizer "não faça isso", mas as pessoas (inclusive eu!) Ocasionalmente fazem essas coisas de qualquer maneira para obter alguma funcionalidade oculta, e isso aconteceria. Essa é a minha teoria atual, pelo menos.
Cory Nelson
1
Não é assim que você cria APIs na maioria dos casos, a menos que esteja relacionado à segurança. Eu prefiro apostar em alguma outra explicação, como "manter suas opções abertas para otimizações de desempenho no futuro". Mas ainda assim, o seu é o melhor até agora.
Andrey Tarantsov
Ei, espero que você me desculpe reatribuindo a resposta aceita à pessoa envolvida na implementação. Enquanto vocês dois dizem a mesma coisa, acho que o SO se beneficia da resposta definitiva estar no topo.
Andrey Tarantsov 23/03/2015
10

O CancellationTokené uma estrutura que muitas cópias podem existir devido à transmissão aos métodos.

Os CancellationTokenSourceconjuntos do estado de todos os exemplares de um símbolo quando chamando Cancela fonte.Veja esta página do MSDN

O motivo do design pode ser apenas uma questão de separação de preocupações e a velocidade de uma estrutura.

ER não
fonte
1
Obrigado. Observe que outra resposta informa que tecnicamente (em IL), CancellationTokenSource não define o estado dos tokens; em vez disso, os tokens envolvem uma referência real a CancellationTokenSource e simplesmente acessam-no para verificar o cancelamento. Se alguma coisa, a velocidade só pode ser perdida aqui.
Andrey Tarantsov
+1. Eu não entendo se for um tipo de valor, para que cada método tenha seu próprio valor (valor separado). Então, como um método sabe se um cancelamento foi chamado? NÃO tem nenhuma referência ao TokenSource. todo o método pode ver é um tipo de valor local como "5". você pode explicar ?
Royi Namir
1
@RoyiNamir: Cada CancellationToken tem referência "m_source" privada do tipo CancellationTokenSource
Andreyul
2

A CancellationTokenSourceé a 'coisa' que emite o cancelamento, por qualquer motivo. Ele precisa de uma maneira de 'despachar' esse cancelamento para todos os CancellationTokenque foi emitido. É assim que, por exemplo, o ASP.NET pode cancelar operações quando uma solicitação é abortada. Cada solicitação possui uma CancellationTokenSourceque encaminha o cancelamento a todos os tokens emitidos.

Isso é ótimo para testes de unidade BTW - crie sua própria fonte de token de cancelamento, obtenha um token, chame Cancela fonte e passe o token para o seu código que precisa lidar com o cancelamento.

n8wrl
fonte
Obrigado - mas ambas as responsabilidades (que são muito relacionadas) podem ser atribuídas à mesma classe. Realmente não explica por que eles são separados.
Andrey Tarantsov