ti-enxame.com

Pergunta sobre como encerrar um thread corretamente no .NET

Eu entendo que Thread.Abort () é ruim entre a multiplicidade de artigos que li sobre o tópico, então, atualmente, estou no processo de extrair dos meus abortos para substituí-los por uma maneira mais limpa; e depois de comparar as estratégias de usuário das pessoas aqui no stackoverflow e depois da leitura " Como criar e finalizar threads (Guia de Programação em C #) " do MSDN, que definem uma abordagem muito parecida - que é usar uma estratégia de verificação de abordagem volatile bool, o que é legal, mas ainda tenho algumas perguntas ...

Imediatamente o que se destaca aqui, e se você não tiver um processo simples de trabalho que esteja executando apenas um loop de código de processamento? Por exemplo, para mim, meu processo é um processo de upload de arquivos em segundo plano; na verdade, eu percorro cada arquivo, então é algo e tenho certeza de que poderia adicionar minha while (!_shouldStop) na parte superior, que cobre todas as iterações de loop, mas eu tenho muitos outros processos de negócios que ocorrem antes da próxima iteração do loop, quero que esse procedimento de cancelamento seja rápido; não me diga que preciso espalhá-las enquanto faz um loop a cada 4-5 linhas em toda a minha função de trabalhador ?!

Eu realmente espero que exista uma maneira melhor, alguém poderia me aconselhar se essa é, de fato, a abordagem [e única?] Correta para fazer isso, ou as estratégias que eles usaram no passado para alcançar o que estou procurando.

Obrigado gangue.

Leituras adicionais: Todas essas respostas SO respostas assumem que o thread de trabalho fará um loop. Isso não fica confortável comigo. E se for uma operação em segundo plano linear, mas oportuna ?

58
GONeale

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

Sondar um sinalizador de parada

Você já mencionou este método. Essa é bem comum. Faça verificações periódicas da bandeira em pontos seguros do seu algoritmo e salve quando for sinalizada. A abordagem padrão é marcar a variável volatile. Se isso não for possível ou inconveniente, você poderá usar um lock. Lembre-se, você não pode marcar uma variável local como volatile, portanto, se uma expressão lambda a capturar através de um fechamento, por exemplo, você teria que recorrer a um método diferente para criar a barreira de memória necessária. Não há muito o que dizer sobre esse método.

Use os novos mecanismos de cancelamento no TPL

Isso é semelhante à pesquisa de um sinalizador de parada, exceto que ele usa as novas estruturas de dados de cancelamento no TPL. Ainda é baseado em padrões de cancelamento cooperativos. Você precisa obter um CancellationToken e verificar periodicamente IsCancellationRequested. Para solicitar o cancelamento, chame Cancel no CancellationTokenSource que originalmente forneceu o token. Há muito que você pode fazer com os novos mecanismos de cancelamento. Você pode ler mais sobre aqui .

Use alças de espera

Esse método pode ser útil se o encadeamento do trabalhador exigir a espera em um intervalo específico ou um sinal durante sua operação normal. Você pode Set a ManualResetEvent, por exemplo, para informar ao thread que é hora de parar. Você pode testar o evento usando a função WaitOne que retorna um bool indicando se o evento foi sinalizado. O WaitOne usa um parâmetro que especifica quanto tempo esperar a chamada retornar se o evento não foi sinalizado nesse período de tempo. Você pode usar esta técnica no lugar de Thread.Sleep e obtenha a indicação de parada ao mesmo tempo. Também é útil se houver outras instâncias WaitHandle nas quais o encadeamento talvez precise esperar. Você pode ligar WaitHandle.WaitAny para aguardar qualquer evento (incluindo o evento de parada), tudo em uma chamada. Usar um evento pode ser melhor do que chamar Thread.Interrupt desde que você tenha mais controle sobre o fluxo do programa (Thread.Interrupt lança uma exceção, assim você teria que colocar estrategicamente o try-catch bloqueia para executar qualquer limpeza necessária).

Cenários especializados

Existem vários cenários pontuais que possuem mecanismos de parada muito especializados. Definitivamente, está fora do escopo desta resposta enumerar todas elas (não importa que seja quase impossível). Um bom exemplo do que quero dizer aqui é a classe Socket. Se o encadeamento for bloqueado em uma chamada para Send ou Receive, a chamada Close interromperá o soquete em qualquer chamada de bloqueio em que foi efetivamente desbloqueada. Tenho certeza de que existem várias outras áreas na BCL em que técnicas semelhantes podem ser usadas para desbloquear um encadeamento.

Interrompa o encadeamento 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 estão os pontos seguros no seu algoritmo. O motivo é porque Thread.Interrupt funciona injetando uma exceção em uma das chamadas de bloqueio de BCL enlatadas. Esses incluem Thread.Sleep, WaitHandle.WaitOne, Thread.Join, etc. Portanto, você deve ser sábio sobre onde colocá-los. No entanto, na maioria das vezes o algoritmo determina onde eles vão e isso geralmente é bom de qualquer maneira, especialmente se o algoritmo passa a maior parte do tempo em uma dessas chamadas de bloqueio. Se o seu 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 e, portanto, é provável em um ponto seguro. No mínimo, você sabe que o encadeamento não pode estar em código não gerenciado ou sair de uma seção crítica, deixando um bloqueio danificado em um estado adquirido. Apesar de ser menos invasivo do que Thread.Abort Ainda desencorajo seu uso porque não é óbvio quais chamadas respondem a ele e muitos desenvolvedores não estão familiarizados com suas nuances.

101
Brian Gideon

Bem, infelizmente no multithreading, você muitas vezes precisa comprometer o "snappiness" para limpeza ... você pode sair de um thread imediatamente se você Interrupt, mas não será muito limpo. Então não, você não precisa borrifar o _shouldStop verifica a cada 4-5 linhas, mas se você interromper o encadeamento, deverá lidar com a exceção e sair do loop de maneira limpa.

Atualizar

Mesmo que não seja um segmento em loop (por exemplo, talvez seja um segmento que execute alguma operação assíncrona de longa duração ou algum tipo de bloco para operação de entrada), você pode Interrupt, mas ainda deve pegar o ThreadInterruptedException e saia da linha de maneira limpa. Eu acho que os exemplos que você está lendo são muito apropriados.

Atualização 2.0

Sim, eu tenho um exemplo ... Vou apenas mostrar um exemplo com base no link que você referenciou:

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);
    }
}
12
Kiril

Se o cancelamento é um requisito do que você está construindo, ele deve ser tratado com tanto respeito quanto o resto do seu código - pode ser algo para o qual você tenha que projetar.

Vamos supor que seu segmento esteja fazendo uma de duas coisas o tempo todo.

  1. Algo vinculado à CPU
  2. Aguardando o kernel

Se sua CPU está vinculada ao encadeamento em questão, você provavelmente tem um bom local para inserir a verificação de resgate. Se você estiver chamando o código de outra pessoa para executar alguma tarefa vinculada à CPU de longa execução, poderá ser necessário corrigir o código externo, removê-lo do processo (interromper threads é ruim, mas interromper processos é bem definido e seguro ) etc.

Se você está aguardando o kernel, provavelmente há um identificador (ou porta fd ou mach ...) envolvido na espera. Normalmente, se você destruir o identificador relevante, o kernel retornará imediatamente com algum código de falha. Se você estiver em .net/Java/etc. você provavelmente acabará com uma exceção. Em C, qualquer código que você já possua para lidar com falhas de chamada do sistema propagará o erro até uma parte significativa do seu aplicativo. De qualquer maneira, você sai do local de baixo nível de maneira bastante limpa e em tempo hábil, sem precisar de um novo código espalhado por toda parte.

Uma tática que costumo usar com esse tipo de código é acompanhar uma lista de identificadores que precisam ser fechados e, em seguida, fazer com que a minha função de cancelamento defina um sinalizador "cancelado" e feche-os. Quando a função falha, pode verificar o sinalizador e reportar a falha devido ao cancelamento, e não devido à exceção/erro específico.

Você parece estar sugerindo que uma granularidade aceitável para cancelamento está no nível de uma chamada de serviço. Provavelmente, isso não é um bom pensamento - é muito melhor cancelar o trabalho em segundo plano de forma síncrona e ingressar no thread de segundo plano antigo a partir do thread de primeiro plano. É muito mais limpo porque:

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

  2. Evita possíveis vazamentos de thread/memória ocultos causados ​​por processos em segundo plano suspensos, possibilitando a ocultação dos efeitos de um thread em 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 um requisito do seu aplicativo, a decisão que você realmente precisa tomar é uma decisão de recursos/negócios: faça um hack ou corrija seu problema de maneira 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 movê-lo para fora de processo. Você obtém um isolamento muito melhor de muitos tipos de riscos, incluindo este, dessa maneira.

9
blucz

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

Você poderia fazer algo assim:

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

Portanto, sim, ele faz loop infinito, mas verifica se é hora de parar entre cada etapa do negócio.

2
Brent Arias

Você não precisa polvilhar enquanto loops em todos os lugares. O loop while externo apenas verifica se foi dito para parar e, se sim, não faz outra iteração ...

Se você tiver um encadeamento "vá fazer algo e feche" (sem loops), basta verificar o booleano _shouldStop antes ou depois de cada ponto principal dentro do encadeamento. Dessa forma, você sabe se deve continuar ou sair.

por exemplo:

public void DoWork() {

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

2
NotMe

A melhor resposta depende muito do que você está fazendo no segmento.

  • Como você disse, a maioria das respostas gira em torno da pesquisa de um booleano compartilhado a cada duas linhas. Mesmo que você não goste, esse geralmente é o esquema mais simples. Se você deseja facilitar sua vida, você pode escrever um método como ThrowIfCancelled (), que lança algum tipo de exceção se você terminar. Os puristas dirão que isto é (suspiro) usando exceções para o fluxo de controle, mas, novamente, o agrupamento é uma imo excepcional.

  • Se você estiver executando operações IO (como coisas de rede)), considere 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<object> 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<object> 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, 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 Timer.tick e anule completamente a segmentação, se fizer sentido.

e faça tudo o que a blucz diz também.

2
Glenn

Em vez de adicionar um loop while no qual um loop não pertence, adicione algo como if (_shouldStop) CleanupAndExit(); sempre que fizer sentido. Não é necessário verificar após cada operação ou espalhar o código por toda parte. Em vez disso, pense em cada verificação como uma chance de sair do encadeamento nesse ponto e adicione-as estrategicamente com isso em mente.

2
Adrian Lopez

Todas essas respostas SO assumem que o thread do trabalhador fará um loop. Isso não fica à vontade comigo

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

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

1
Hans Passant