Maneira mais simples de fazer um método de fogo e esquecer em c # 4.0

100

Eu realmente gosto desta pergunta:

Maneira mais simples de fazer um método de fogo e esquecer em C #?

Eu só quero saber que agora que temos extensões paralelas em C # 4.0, há uma maneira melhor e mais limpa de fazer Fire & Forget com Parallel linq?

Jonathon Kresner
fonte
1
A resposta a essa pergunta ainda se aplica ao .NET 4.0. Disparar e esquecer não fica muito mais simples do que QueueUserWorkItem.
Brian Rasmussen,

Respostas:

108

Não é uma resposta para 4.0, mas vale a pena notar que no .Net 4.5 você pode tornar isso ainda mais simples com:

#pragma warning disable 4014
Task.Run(() =>
{
    MyFireAndForgetMethod();
}).ConfigureAwait(false);
#pragma warning restore 4014

O pragma é desativar o aviso que informa que você está executando esta tarefa como disparar e esquecer.

Se o método dentro das chaves retornar uma Tarefa:

#pragma warning disable 4014
Task.Run(async () =>
{
    await MyFireAndForgetMethod();
}).ConfigureAwait(false);
#pragma warning restore 4014

Vamos decompô-lo:

Task.Run retorna uma Tarefa, que gera um aviso do compilador (aviso CS4014) observando que esse código será executado em segundo plano - é exatamente o que você queria, portanto, desabilitamos o aviso 4014.

Por padrão, as Tarefas tentam "Marshal de volta para o Thread original", o que significa que esta Tarefa será executada em segundo plano e, em seguida, tentará retornar ao Thread que a iniciou. Freqüentemente, dispare e esqueça as Tarefas terminam depois que o Thread original é concluído. Isso fará com que uma ThreadAbortException seja lançada. Na maioria dos casos, isso é inofensivo - é apenas dizer a você, eu tentei voltar, falhei, mas você não se importa de qualquer maneira. Mas ainda é um pouco barulhento ter ThreadAbortExceptions em seus logs em produção ou em seu depurador no dev local. .ConfigureAwait(false)é apenas uma maneira de ficar organizado e dizer explicitamente, execute isso em segundo plano e é isso.

Como isso é prolixo, especialmente o pragma feio, uso um método de biblioteca para isso:

public static class TaskHelper
{
    /// <summary>
    /// Runs a TPL Task fire-and-forget style, the right way - in the
    /// background, separate from the current thread, with no risk
    /// of it trying to rejoin the current thread.
    /// </summary>
    public static void RunBg(Func<Task> fn)
    {
        Task.Run(fn).ConfigureAwait(false);
    }

    /// <summary>
    /// Runs a task fire-and-forget style and notifies the TPL that this
    /// will not need a Thread to resume on for a long time, or that there
    /// are multiple gaps in thread use that may be long.
    /// Use for example when talking to a slow webservice.
    /// </summary>
    public static void RunBgLong(Func<Task> fn)
    {
        Task.Factory.StartNew(fn, TaskCreationOptions.LongRunning)
            .ConfigureAwait(false);
    }
}

Uso:

TaskHelper.RunBg(async () =>
{
    await doSomethingAsync();
}
Chris Moschini
fonte
7
@ksm Infelizmente sua abordagem terá alguns problemas - você a testou? Essa abordagem é exatamente a razão pela qual o Aviso 4014 existe. Chamar um método assíncrono sem esperar e sem a ajuda de Task.Run ... fará com que esse método seja executado, sim, mas assim que terminar, tentará empacotar de volta ao encadeamento original do qual foi disparado. Freqüentemente, esse thread já terá a execução concluída e seu código explodirá de maneiras confusas e indeterminadas. Não faça isso! A chamada para Task.Run é uma maneira conveniente de dizer "Execute isso no contexto global", não deixando nada para tentar empacotar.
Chris Moschini
1
@ChrisMoschini fico feliz em ajudar e obrigado pela atualização! Por outro lado, onde escrevi "você está chamando com uma função anônima assíncrona" no comentário acima, é honestamente confuso para mim qual é a verdade. Tudo que sei é que o código funciona quando o assíncrono não está incluído no código de chamada (função anônima), mas isso significaria que o código de chamada não seria executado de forma assíncrona (o que é ruim, um péssimo pegadinho)? Então, eu estou não recomendar sem a assíncrona, é apenas estranho que, neste caso, tanto trabalho.
Nicholas Petersen,
8
Eu não vejo o ponto de ConfigureAwait(false)em Task.Runquando você não faz awaita tarefa. O objetivo da função está em seu nome: "configure await ". Se você não fizer awaita tarefa, não registrará uma continuação e não haverá nenhum código para a tarefa "empacotar de volta ao thread original", como você diz. O maior risco é de uma exceção não observada ser relançada no encadeamento do finalizador, que esta resposta nem mesmo aborda.
Mike Strobel
3
@stricq Não há usos de void assíncronos aqui. Se você estiver se referindo a async () => ... a assinatura é um Func que retorna uma Tarefa, não void.
Chris Moschini
2
No VB.net para suprimir o aviso:#Disable Warning BC42358
Altiano Gerung
85

Com a Taskclasse sim, mas PLINQ é realmente para consultar coleções.

Algo como o seguinte fará isso com a Tarefa.

Task.Factory.StartNew(() => FireAway());

Ou mesmo ...

Task.Factory.StartNew(FireAway);

Ou...

new Task(FireAway).Start();

Onde FireAwayestá

public static void FireAway()
{
    // Blah...
}

Então, em virtude da concisão do nome da classe e do método, isso supera a versão do threadpool por entre seis e dezenove caracteres, dependendo do que você escolher :)

ThreadPool.QueueUserWorkItem(o => FireAway());
Ade Miller
fonte
Certamente eles não são equivalentes de funcionalidade?
Jonathon Kresner,
6
Há uma diferença semântica sutil entre StartNew e new Task.Start, mas, caso contrário, sim. Todos eles enfileiram FireAway para executar em um thread no threadpool.
Ade Miller
Trabalhando para este caso fire and forget in ASP.NET WebForms and windows.close():?
PreguntonCojoneroCabrón
32

Eu tenho alguns problemas com a resposta principal a esta pergunta.

Em primeiro lugar, em uma situação verdadeira de fogo e esquecimento , você provavelmente não awaitexecutará a tarefa, por isso é inútil anexar ConfigureAwait(false). Se você não awaitdefinir o valor retornado por ConfigureAwait, ele não terá nenhum efeito.

Em segundo lugar, você precisa estar ciente do que acontece quando a tarefa é concluída com uma exceção. Considere a solução simples que @ ade-miller sugeriu:

Task.Factory.StartNew(SomeMethod);  // .NET 4.0
Task.Run(SomeMethod);               // .NET 4.5

Isso introduz um perigo: se uma exceção não tratada escapa SomeMethod(), nunca será observado que exceção, e podem 1 seja relançada no segmento finalizador, batendo sua aplicação. Portanto, eu recomendaria usar um método auxiliar para garantir que quaisquer exceções resultantes sejam observadas.

Você poderia escrever algo assim:

public static class Blindly
{
    private static readonly Action<Task> DefaultErrorContinuation =
        t =>
        {
            try { t.Wait(); }
            catch {}
        };

    public static void Run(Action action, Action<Exception> handler = null)
    {
        if (action == null)
            throw new ArgumentNullException(nameof(action));

        var task = Task.Run(action);  // Adapt as necessary for .NET 4.0.

        if (handler == null)
        {
            task.ContinueWith(
                DefaultErrorContinuation,
                TaskContinuationOptions.ExecuteSynchronously |
                TaskContinuationOptions.OnlyOnFaulted);
        }
        else
        {
            task.ContinueWith(
                t => handler(t.Exception.GetBaseException()),
                TaskContinuationOptions.ExecuteSynchronously |
                TaskContinuationOptions.OnlyOnFaulted);
        }
    }
}

Essa implementação deve ter sobrecarga mínima: a continuação só é chamada se a tarefa não for concluída com êxito e deve ser chamada de forma síncrona (em vez de ser agendada separadamente da tarefa original). No caso "preguiçoso", você nem mesmo incorrerá em uma alocação para o delegado de continuação.

Iniciar uma operação assíncrona torna-se trivial:

Blindly.Run(SomeMethod);                              // Ignore error
Blindly.Run(SomeMethod, e => Log.Warn("Whoops", e));  // Log error

1. Esse era o comportamento padrão no .NET 4.0. No .NET 4.5, o comportamento padrão foi alterado de forma que exceções não observadas não fossem relançadas no encadeamento do finalizador (embora você ainda possa observá-las por meio do evento UnobservedTaskException em TaskScheduler). No entanto, a configuração padrão pode ser substituída e, mesmo se seu aplicativo exigir .NET 4.5, você não deve presumir que as exceções de tarefas não observadas serão inofensivas.

Mike Strobel
fonte
1
Se um manipulador for passado e o ContinueWithfor invocado devido ao cancelamento, a propriedade de exceção do antecedente será Null e uma exceção de referência nula será lançada. Definir a opção de continuação de tarefa como OnlyOnFaultedeliminará a necessidade de verificação de nulo ou verificará se a exceção é nula antes de usá-la.
sanmcp
O método Run () precisa ser substituído por diferentes assinaturas de método. Melhor fazer isso como um método de extensão de Task.
stricq
1
@stricq A pergunta feita sobre as operações "disparar e esquecer" (ou seja, status nunca verificado e resultado nunca observado), então foi nisso que me concentrei. Quando a questão é como dar um tiro no próprio pé da maneira mais limpa, debater qual solução é "melhor" do ponto de vista do design torna-se bastante discutível :). A melhor resposta é indiscutivelmente "não", mas isso ficou aquém do comprimento mínimo de resposta, então me concentrei em fornecer uma solução concisa que evita problemas presentes em outras respostas. Os argumentos são facilmente agrupados em um encerramento e sem nenhum valor de retorno, using Tasktorna-se um detalhe de implementação.
Mike Strobel
@stricq Dito isso, não discordo de você. A alternativa que você propôs é exatamente o que fiz no passado, embora por motivos diferentes. "Quero evitar travar o aplicativo quando um desenvolvedor deixar de observar uma falha Task" não é o mesmo que "Quero uma maneira simples de iniciar uma operação em segundo plano, nunca observe seu resultado e não me importo com o mecanismo usado para fazer isso. " Com base nisso, mantenho minha resposta à pergunta que foi feita .
Mike Strobel
Trabalhando para este caso: fire and forgetem ASP.NET WebForms e windows.close()?
PreguntonCojoneroCabrón
7

Apenas para corrigir algum problema que vai acontecer com a resposta de Mike Strobel:

Se você usar var task = Task.Run(action)e depois de atribuir uma continuação a essa tarefa, correrá o risco de Tasklançar alguma exceção antes de atribuir uma continuação do manipulador de exceção ao Task. Portanto, a classe abaixo deve estar livre deste risco:

using System;
using System.Threading.Tasks;

namespace MyNameSpace
{
    public sealed class AsyncManager : IAsyncManager
    {
        private Action<Task> DefaultExeptionHandler = t =>
        {
            try { t.Wait(); }
            catch { /* Swallow the exception */ }
        };

        public Task Run(Action action, Action<Exception> exceptionHandler = null)
        {
            if (action == null) { throw new ArgumentNullException(nameof(action)); }

            var task = new Task(action);

            Action<Task> handler = exceptionHandler != null ?
                new Action<Task>(t => exceptionHandler(t.Exception.GetBaseException())) :
                DefaultExeptionHandler;

            var continuation = task.ContinueWith(handler,
                TaskContinuationOptions.ExecuteSynchronously
                | TaskContinuationOptions.OnlyOnFaulted);
            task.Start();

            return continuation;
        }
    }
}

Aqui, o tasknão é executado diretamente, em vez disso, é criado, uma continuação é atribuída, e somente então a tarefa é executada para eliminar o risco de a tarefa completar a execução (ou lançar alguma exceção) antes de atribuir uma continuação.

O Runmétodo aqui retorna a continuação, Taskentão posso escrever testes de unidade, certificando-se de que a execução seja concluída. Você pode ignorá-lo com segurança em seu uso.

Mert Akcakaya
fonte
Também é intencional que você não a tornou uma classe estática?
Deantwo
Esta classe implementa a interface IAsyncManager, portanto, não pode ser uma classe estática.
Mert Akcakaya