Pergunta sobre terminar um thread de forma limpa no .NET

Eu entendo Thread.Abort () é mal da multiplicidade de artigos que eu li sobre o tema, então eu estou atualmente no processo de rasgar para fora do meu aborto, a fim de substituí-lo por um caminho mais limpo; e depois de comparar as estratégias de usuários das pessoas aqui no stackoverflow e depois de ler ” Como: Criar e Terminar Threads (Guia de Programação C #) “ do MSDN ambos afirmam uma abordagem muito parecida – que é usar uma verificação volatile bool abordagem booleana estratégia, o que é legal, mas ainda tenho algumas perguntas ….

Imediatamente o que se destaca para mim aqui, é o que se você não tem um processo de trabalho simples que está apenas executando um loop de código de mastigação? Por exemplo, para mim, meu processo é um processo de upload de arquivos em segundo plano, eu de fato passo através de cada arquivo, isso é algo, e com certeza eu poderia adicionar meu while (!_shouldStop) no topo que me cobre toda iteração de loop, mas eu tem muitos mais processos de negócios que ocorrem antes de atingir sua próxima iteração de loop, eu quero que este procedimento de cancelamento seja rápido; não me diga que eu preciso borrifar esses loops a cada 4-5 linhas ao longo de toda a minha function de trabalho ?!

Eu realmente espero que haja uma maneira melhor, alguém poderia me aconselhar sobre se isso é de fato, a abordagem correta [e única?] Para fazer isso, ou estratégias que eles usaram no passado para alcançar o que eu sou depois.

Obrigado gangue.

Leitura adicional: Todas essas respostas de SO supõem que o thread de trabalho será executado em loop. Isso não se senta confortavelmente comigo. E se for uma operação de plano de fundo linear, mas oportuna?

   

Infelizmente, pode não haver uma opção melhor. Isso realmente depende do seu cenário específico. A ideia é parar o fio graciosamente em pontos seguros. Esse é o cerne da razão porque Thread.Abort não é bom; porque não é garantido que ocorra em pontos seguros. Ao polvilhar o código com um mecanismo de parada, você está efetivamente definindo manualmente os pontos seguros. Isso é chamado de cancelamento cooperativo. Existem basicamente 4 mecanismos amplos para fazer isso. Você pode escolher o que melhor se adapta à sua situação.

Poll uma bandeira de parada

Você já mencionou este método. Isso é bem comum. Faça verificações periódicas da bandeira em pontos seguros no seu algoritmo e resista quando for sinalizado. A abordagem padrão é marcar a variável volatile . Se isso não for possível ou inconveniente, então você pode usar um lock . Lembre-se, você não pode marcar uma variável local como volatile portanto, se uma expressão lambda a capturar por meio de um encerramento, por exemplo, você precisará recorrer a um método diferente para criar a barreira de memory necessária. Não há muito mais que precise ser dito sobre esse método.

Use os novos mecanismos de cancelamento no TPL

Isso é semelhante a pesquisar uma sinalização de parada, exceto que ela usa as novas estruturas de dados de cancelamento na TPL. Ainda é baseado em padrões de cancelamento cooperativo. Você precisa obter um CancellationToken e verificar periodicamente IsCancellationRequested . Para solicitar o cancelamento, você chamaria Cancel no CancellationTokenSource que originalmente forneceu o token. Há muito o que fazer com os novos mecanismos de cancelamento. Você pode ler mais sobre aqui .

Use as alças de espera

Esse método pode ser útil se seu encadeamento de trabalhador precisar aguardar em um intervalo específico ou por um sinal durante sua operação normal. Você pode Set um ManualResetEvent , por exemplo, para permitir que o thread saiba que está na hora de parar. Você pode testar o evento usando a function WaitOne que retorna um bool indicando se o evento foi sinalizado. O WaitOne usa um parâmetro que especifica quanto tempo esperar pela chamada retornar se o evento não foi sinalizado naquele período de tempo. Você pode usar essa técnica no lugar de Thread.Sleep e obter a indicação de parada ao mesmo tempo. Também é útil se houver outras instâncias WaitHandle que o thread pode ter que esperar. Você pode chamar WaitHandle.WaitAny para esperar em qualquer evento (incluindo o evento de parada) em uma única chamada. Usando um evento pode ser melhor do que chamar Thread.Interrupt desde que você tenha mais controle sobre o stream do programa ( Thread.Interrupt gera uma exceção, portanto, você teria que colocar estrategicamente os blocos try-catch para executar qualquer limpeza necessária).

Cenários especializados

Existem vários cenários pontuais que possuem mecanismos de parada muito especializados. É definitivamente fora do escopo desta resposta enumerar todos eles (não importa que seja quase impossível). Um bom exemplo do que quero dizer aqui é a class Socket . Se o encadeamento estiver bloqueado em uma chamada para Send ou Receive , a chamada de Close interromperá o soquete em qualquer chamada de bloqueio em que ele foi efetivamente desbloqueado. Eu tenho certeza que existem várias outras áreas no BCL onde técnicas similares podem ser usadas para desbloquear um thread.

Interromper o segmento via Thread.Interrupt

A vantagem aqui é que é simples e você não precisa se concentrar em espalhar seu código com algo realmente. A desvantagem é que você tem pouco controle sobre onde os pontos seguros estão em seu algoritmo. O motivo é porque o Thread.Interrupt funciona injetando uma exceção dentro de uma das chamadas de bloqueio da BCL. Estes incluem Thread.Sleep , WaitHandle.WaitOne , Thread.Join , etc Então você tem que ser sábio sobre onde você colocá-los. No entanto, a maior parte do tempo em que o algoritmo dita aonde eles vão e isso geralmente é bom, especialmente se seu algoritmo passa a maior parte do tempo em uma dessas chamadas de bloqueio. Se o algoritmo não usar uma das chamadas de bloqueio na BCL, esse método não funcionará para você. A teoria aqui é que o ThreadInterruptException é gerado apenas a partir da chamada em espera do .NET, portanto é provável que seja em um ponto seguro. No mínimo, você sabe que o segmento não pode estar em código não gerenciado ou sair de uma seção crítica deixando um bloqueio pendente em um estado adquirido. Apesar de ser menos invasivo que o Thread.Abort eu ainda desestimulo seu uso porque não é óbvio quais chamadas respondem a ele e muitos desenvolvedores não estão familiarizados com suas nuances.

Bem, infelizmente em multithreading você freqüentemente tem que comprometer o “snappiness” para limpeza … você pode sair de um thread imediatamente se você o Interrupt , mas não será muito limpo. Portanto, não, você não precisa borrifar o _shouldStop verifica a cada 4-5 linhas, mas se você interromper o seu thread, então você deve lidar com a exceção e sair do loop de uma maneira limpa.

Atualizar

Mesmo que não seja um encadeamento em loop (por exemplo, talvez seja um encadeamento que execute alguma operação assíncrona de longa execução ou algum tipo de bloco para operação de input), você pode Interrupt lo, mas ainda deve capturar o ThreadInterruptedException e sair do encadeamento. Eu acho que os exemplos que você está lendo são muito apropriados.

Atualização 2.0

Sim, eu tenho um exemplo … Vou mostrar um exemplo baseado no link que você mencionou:

 public class InterruptExample { private Thread t; private volatile boolean alive; public InterruptExample() { alive = false; t = new Thread(()=> { try { while (alive) { /* Do work. */ } } catch (ThreadInterruptedException exception) { /* Clean up. */ } }); t.IsBackground = true; } public void Start() { alive = true; t.Start(); } public void Kill(int timeout = 0) { // somebody tells you to stop the thread t.Interrupt(); // Optionally you can block the caller // by making them wait until the thread exits. // If they leave the default timeout, // then they will not wait at all t.Join(timeout); } } 

Se o cancelamento é uma exigência da coisa que você está construindo, então ele deve ser tratado com tanto respeito quanto o resto do seu código – pode ser algo que você tenha que projetar.

Vamos supor que o seu segmento está fazendo uma das duas coisas em todos os momentos.

  1. Algo CPU ligado
  2. Esperando pelo kernel

Se você está ligado à CPU no tópico em questão, você provavelmente tem um bom local para inserir a verificação de socorro. Se você está chamando o código de outra pessoa para executar uma tarefa de CPU de longa execução, talvez seja necessário corrigir o código externo, movê-lo para fora do processo (abortar threads é ruim, mas abortar processos é bem definido e seguro ), etc.

Se você está esperando pelo kernel, então provavelmente há uma alça (ou fd, ou mach port, …) envolvida na espera. Normalmente, se você destruir o identificador relevante, o kernel retornará imediatamente com algum código de falha. Se você está em .net / java / etc. você provavelmente acabará com uma exceção. Em C, o código que você já possui para lidar com falhas de chamadas do sistema irá propagar o erro para uma parte significativa do seu aplicativo. De qualquer forma, você sai do local de baixo nível de forma bastante clara e de maneira muito oportuna, sem precisar de novo código espalhado por toda parte.

Uma tática que eu geralmente uso com esse tipo de código é manter o controle de uma lista de alças que precisam ser fechadas e então fazer com que minha function de aborto defina um sinalizador “cancelado” e depois fechá-las. Quando a function falha, pode verificar o sinalizador e reportar falha devido ao cancelamento, e não devido a qualquer exceção específica / errno.

Você parece estar insinuando que uma granularidade aceitável para cancelamento está no nível de uma chamada de serviço. Isso provavelmente não é bom pensar – é muito melhor cancelar o trabalho em segundo plano de forma síncrona e unir o antigo thread de segundo plano do thread em primeiro plano. É mais limpo porque:

  1. Evita uma class de condições de corrida quando antigos segmentos de bgwork voltam à vida após atrasos inesperados.

  2. Isso evita possíveis vazamentos de memory / encadeamento ocultos causados ​​por processos de fundo suspensos, possibilitando a ocultação dos efeitos de um encadeamento de segundo plano suspenso.

Há duas razões para ter medo dessa abordagem:

  1. Você não acha que pode abortar seu próprio código em tempo hábil. Se o cancelamento for uma exigência do seu aplicativo, a decisão que você realmente precisa tomar é uma decisão de recurso / negócio: faça um hack ou corrija seu problema de forma limpa.

  2. Você não confia em algum código que está chamando porque está fora de seu controle. Se você realmente não confia, considere transferi-lo fora do processo. Você obtém isolamento muito melhor de muitos tipos de riscos, incluindo esse, dessa maneira.

Talvez a parte do problema seja que você tem um método / loop tão longo. Esteja ou não tendo problemas de encadeamento, você deve dividi-lo em etapas de processamento menores. Vamos supor que essas etapas sejam Alpha (), Bravo (), Charlie () e Delta ().

Você poderia então fazer algo assim:

  public void MyBigBackgroundTask() { Action[] tasks = new Action[] { Alpha, Bravo, Charlie, Delta }; int workStepSize = 0; while (!_shouldStop) { tasks[workStepSize++](); workStepSize %= tasks.Length; }; } 

Então, sim, ele faz um loop infinitamente, mas verifica se é hora de parar entre cada etapa do negócio.

Você não precisa polvilhar loops em todos os lugares. O loop while externo apenas verifica se ele foi instruído a parar e, em caso afirmativo, não faz outra iteração …

Se você tiver um thread direto “vai fazer algo e fechar” (sem loops nele), então basta verificar o booleano _shouldStop antes ou depois de cada ponto principal dentro do encadeamento. Dessa forma, você sabe se deve continuar ou salvar.

por exemplo:

 public void DoWork() { RunSomeBigMethod(); if (_shouldStop){ return; } RunSomeOtherBigMethod(); if (_shouldStop){ return; } //.... } 

A melhor resposta depende em grande parte do que você está fazendo no tópico.

  • Como você disse, a maioria das respostas gira em torno de sondar um booleano compartilhado a cada duas linhas. Mesmo que você não goste, esse é o esquema mais simples. Se você quiser tornar sua vida mais fácil, você pode escrever um método como ThrowIfCancelled (), que lança algum tipo de exceção se você terminar. Os puristas dirão que isso é (suspiro) usando exceções para o stream de controle, mas, novamente, a cobrança é excepcional.

  • Se você estiver fazendo operações de E / S (como coisas de rede), você pode querer considerar fazer tudo usando operações assíncronas.

  • Se você estiver executando uma sequência de etapas, poderá usar o truque IEnumerable para criar uma máquina de estado. Exemplo:

<

 abstract class StateMachine : IDisposable { public abstract IEnumerable Main(); public virtual void Dispose() { /// ... override with free-ing code ... } bool wasCancelled; public bool Cancel() { // ... set wasCancelled using locking scheme of choice ... } public Thread Run() { var thread = new Thread(() => { try { if(wasCancelled) return; foreach(var x in Main()) { if(wasCancelled) return; } } finally { Dispose(); } }); thread.Start() } } class MyStateMachine : StateMachine { public override IEnumerabl Main() { DoSomething(); yield return null; DoSomethingElse(); yield return null; } } // then call new MyStateMachine().Run() to run. 

>

Overengineering? Depende de quantas máquinas de estado você usa. Se você tiver apenas 1, sim. Se você tem 100, então talvez não. Muito complicado? Bem, isto depende. Outro bônus dessa abordagem é que ela permite que você (com pequenas modificações) mova sua operação para um retorno de chamada do Timer.tick e anule completamente o encadeamento, se fizer sentido.

e fazer tudo o que o blucz diz também.

Em vez de adicionar um loop while ao qual um loop não pertence, adicione algo como if (_shouldStop) CleanupAndExit(); onde quer que faça sentido. Não há necessidade de verificar após cada operação ou borrifar todo o código com elas. Em vez disso, pense em cada cheque como uma chance de sair da discussão nesse ponto e adicioná-los estrategicamente com isso em mente.

Todas essas respostas de SO supõem que o encadeamento do trabalhador fará um loop. Isso não fica confortavelmente comigo

Não há muitas maneiras de fazer o código demorar muito. Looping é uma construção de programação bastante essencial. Fazer com que o código demore muito tempo sem o loop requer uma quantidade enorme de instruções. Centenas de milhares.

Ou chamando algum outro código que está fazendo o loop para você. Sim, é difícil fazer esse código parar por demanda. Isso simplesmente não funciona.