Quando o TaskCompletionSource <T> deve ser usado?

199

AFAIK, tudo o que sabe é que, em algum momento, seu SetResultou SetExceptionmétodo está sendo chamado para concluir o Task<T>exposto por meio de sua Taskpropriedade.

Em outras palavras, ele atua como produtor de uma Task<TResult>e sua conclusão.

Eu vi aqui o exemplo:

Se eu precisar de uma maneira de executar um Func de forma assíncrona e ter uma tarefa para representar essa operação.

public static Task<T> RunAsync<T>(Func<T> function) 
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ => 
    { 
        try 
        {  
            T result = function(); 
            tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
    }); 
    return tcs.Task; 
}

Que poderia ser usado * se eu não tinha Task.Factory.StartNew- Mas eu não tenho Task.Factory.StartNew.

Questão:

Alguém pode explicar por exemplo um cenário relacionado diretamente ao TaskCompletionSource e não a uma hipotética situação em que eu não tenho Task.Factory.StartNew?

Royi Namir
fonte
5
TaskCompletionSource é usado principalmente para agrupar API assíncrona baseada em eventos com a Tarefa sem criar novos Threads.
Arvis

Respostas:

230

Uso-o principalmente quando apenas uma API baseada em eventos está disponível ( por exemplo, soquetes do Windows Phone 8 ):

public Task<Args> SomeApiWrapper()
{
    TaskCompletionSource<Args> tcs = new TaskCompletionSource<Args>(); 

    var obj = new SomeApi();

    // will get raised, when the work is done
    obj.Done += (args) => 
    {
        // this will notify the caller 
        // of the SomeApiWrapper that 
        // the task just completed
        tcs.SetResult(args);
    }

    // start the work
    obj.Do();

    return tcs.Task;
}

Portanto, é especialmente útil quando usado junto com a asyncpalavra-chave C # 5 .

GameScripting
fonte
4
você pode escrever em palavras o que vemos aqui? é assim que SomeApiWrapperse espera em algum lugar, até que o editor levante o evento que faz com que essa tarefa seja concluída?
Royi Namir 9/03/2013
dê uma olhada no link que acabei de adicionar
GameScripting 9/13
6
Apenas uma atualização, a Microsoft lançou o Microsoft.Bcl.Asyncpacote no NuGet, que permite as async/awaitpalavras - chave em projetos .NET 4.0 (recomenda-se o VS2012 e superior).
Erik
1
@ Fran_gg7 você poderia usar um CancellationToken, consulte msdn.microsoft.com/en-us/library/dd997396(v=vs.110).aspx ou como uma nova pergunta aqui no stackoverflow
GameScripting
1
O problema com esta implementação é que isso gera um vazamento de memória, pois o evento nunca é liberado da obj
Feito #
78

Em minhas experiências, TaskCompletionSourceé ótimo para agrupar antigos padrões assíncronos no async/awaitpadrão moderno .

O exemplo mais benéfico em que consigo pensar é quando trabalho Socket. Ele possui os antigos padrões APM e EAP, mas não os awaitable Taskmétodos que possuem TcpListenere TcpClientpossuem.

Pessoalmente, tenho vários problemas com a NetworkStreamturma e prefiro o cru Socket. Como também amo o async/awaitpadrão, criei uma classe de extensão SocketExtenderque cria vários métodos de extensão Socket.

Todos esses métodos são usados TaskCompletionSource<T>para agrupar as chamadas assíncronas da seguinte maneira:

    public static Task<Socket> AcceptAsync(this Socket socket)
    {
        if (socket == null)
            throw new ArgumentNullException("socket");

        var tcs = new TaskCompletionSource<Socket>();

        socket.BeginAccept(asyncResult =>
        {
            try
            {
                var s = asyncResult.AsyncState as Socket;
                var client = s.EndAccept(asyncResult);

                tcs.SetResult(client);
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);
            }

        }, socket);

        return tcs.Task;
    }

I passar a socketnos BeginAcceptmétodos para que eu recebo um ligeiro aumento de desempenho fora do compilador não ter que içar o parâmetro local.

Então a beleza de tudo:

 var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
 listener.Bind(new IPEndPoint(IPAddress.Loopback, 2610));
 listener.Listen(10);

 var client = await listener.AcceptAsync();
Erik
fonte
1
Por que Task.Factory.StartNew não funcionou aqui?
Tola Odejayi
23
@Tola Como isso teria criado uma nova tarefa em execução em um encadeamento de encadeamentos, mas o código acima utiliza o encadeamento de conclusão de E / S iniciado por BeginAccept, iow: ele não inicia um novo encadeamento.
Frans Bouma
4
Obrigado, @ Frans-Bouma. Portanto, TaskCompletionSource é uma maneira prática de converter código que usa as instruções Begin ... End ... em uma tarefa?
Tola Odejayi
3
@TolaOdejayi Resposta tardia, mas sim, esse é um dos principais casos de uso que encontrei para ela. Funciona maravilhosamente para essa transição de código.
Erik
4
Veja o TaskFactory <TResult> .FromAsync para quebrar as Begin.. End...instruções.
MicBig 02/10/19
37

Para mim, um cenário clássico para o uso TaskCompletionSourceé quando é possível que meu método não precise necessariamente fazer uma operação demorada. O que isso nos permite fazer é escolher os casos específicos em que gostaríamos de usar um novo thread.

Um bom exemplo disso é quando você usa um cache. Você pode ter um GetResourceAsyncmétodo que procure no cache o recurso solicitado e retorne imediatamente (sem usar um novo encadeamento, usando TaskCompletionSource) se o recurso foi encontrado. Somente se o recurso não for encontrado, gostaríamos de usar um novo thread e recuperá-lo usando Task.Run().

Um exemplo de código pode ser visto aqui: Como executar um código condicionalmente de forma assíncrona usando tarefas

Adi Lester
fonte
Eu vi sua pergunta e também a resposta. (veja meu comentário na resposta) .... :-) e, de fato, é uma pergunta e resposta educativa.
Royi Namir
11
Na verdade, essa não é uma situação na qual o TCS é necessário. Você pode simplesmente usar Task.FromResultpara fazer isso. Obviamente, se você estiver usando o 4.0 e não tiver um para o Task.FromResultqual você usaria um TCS, escreva o seu FromResult .
Servy
O @Servy Task.FromResultestá disponível apenas desde o .NET 4.5. Antes disso, era assim que se conseguia esse comportamento.
Adi Lester
@AdiLester Sua resposta é referenciar Task.Run, indicando que é 4,5+. E meu comentário anterior abordou especificamente o .NET 4.0.
Servy
@Servy Nem todo mundo que lê esta resposta está direcionando o .NET 4.5+. Acredito que esta é uma resposta boa e válida que ajuda as pessoas a fazer a pergunta do OP (que, a propósito, está marcada como .NET-4.0). De qualquer forma, reduzir a votação parece um pouco demais para mim, mas se você realmente acredita que merece uma votação negativa, vá em frente.
Adi Lester
25

Em este post , Levi Botelho descreve como usar a TaskCompletionSourceescrever um invólucro assíncrona para um processo de tal forma que você pode lançá-lo e aguardar a sua rescisão.

public static Task RunProcessAsync(string processPath)
{
    var tcs = new TaskCompletionSource<object>();
    var process = new Process
    {
        EnableRaisingEvents = true,
        StartInfo = new ProcessStartInfo(processPath)
        {
            RedirectStandardError = true,
            UseShellExecute = false
        }
    };
    process.Exited += (sender, args) =>
    {
        if (process.ExitCode != 0)
        {
            var errorMessage = process.StandardError.ReadToEnd();
            tcs.SetException(new InvalidOperationException("The process did not exit correctly. " +
                "The corresponding error message was: " + errorMessage));
        }
        else
        {
            tcs.SetResult(null);
        }
        process.Dispose();
    };
    process.Start();
    return tcs.Task;
}

e seu uso

await RunProcessAsync("myexecutable.exe");
Sarin
fonte
14

Parece que ninguém foi mencionado, mas acho que os testes de unidade também podem ser considerados suficientes na vida real .

Acho TaskCompletionSourceútil quando zombando de uma dependência com um método assíncrono.

No programa real em teste:

public interface IEntityFacade
{
  Task<Entity> GetByIdAsync(string id);
}

Nos testes de unidade:

// set up mock dependency (here with NSubstitute)

TaskCompletionSource<Entity> queryTaskDriver = new TaskCompletionSource<Entity>();

IEntityFacade entityFacade = Substitute.For<IEntityFacade>();

entityFacade.GetByIdAsync(Arg.Any<string>()).Returns(queryTaskDriver.Task);

// later on, in the "Act" phase

private void When_Task_Completes_Successfully()
{
  queryTaskDriver.SetResult(someExpectedEntity);
  // ...
}

private void When_Task_Gives_Error()
{
  queryTaskDriver.SetException(someExpectedException);
  // ...
}

Afinal, esse uso do TaskCompletionSource parece outro caso de "um objeto Task que não executa código".

superjos
fonte
11

TaskCompletionSource é usado para criar objetos de tarefas que não executam código. Em cenários do mundo real, TaskCompletionSource é ideal para operações vinculadas de E / S. Dessa forma, você obtém todos os benefícios das tarefas (por exemplo, valores de retorno, continuações etc.) sem bloquear um encadeamento durante a operação. Se sua "função" for uma operação vinculada a E / S, não é recomendável bloquear um encadeamento usando uma nova tarefa . Em vez disso, usando TaskCompletionSource , você pode criar uma tarefa escrava para indicar apenas quando sua operação de ligação de E / S termina ou falha.

v1p3r
fonte
5

Há um exemplo do mundo real com uma explicação decente nesta postagem do blog "Parallel Programming with .NET" . Você realmente deveria lê-lo, mas aqui está um resumo de qualquer maneira.

A postagem do blog mostra duas implementações para:

"um método de fábrica para criar tarefas" atrasadas ", que não serão realmente agendadas até que ocorra algum tempo limite fornecido pelo usuário."

A primeira implementação mostrada é baseada Task<>e possui duas falhas principais. O segundo post de implementação continua a atenuá-los usando TaskCompletionSource<>.

Aqui está a segunda implementação:

public static Task StartNewDelayed(int millisecondsDelay, Action action)
{
    // Validate arguments
    if (millisecondsDelay < 0)
        throw new ArgumentOutOfRangeException("millisecondsDelay");
    if (action == null) throw new ArgumentNullException("action");

    // Create a trigger used to start the task
    var tcs = new TaskCompletionSource<object>();

    // Start a timer that will trigger it
    var timer = new Timer(
        _ => tcs.SetResult(null), null, millisecondsDelay, Timeout.Infinite);

    // Create and return a task that will be scheduled when the trigger fires.
    return tcs.Task.ContinueWith(_ =>
    {
        timer.Dispose();
        action();
    });
}
urig
fonte
seria melhor usar aguardam em tcs.Task e, em seguida, usar a ação () depois
Royi Namir
5
porque você voltou ao contexto em que saiu, onde o Continuewith não preserva o contexto. (não por padrão) também se a próxima instrução em action () causar uma exceção, seria difícil capturá-la onde o uso de aguardar mostrará uma exceção regular.
Royi Namir
3
Porque não é só await Task.Delay(millisecondsDelay); action(); return;ou (em .Net 4.0)return Task.Delay(millisecondsDelay).ContinueWith( _ => action() );
sgnsajgon
@sgnsajgon que seria certamente mais fácil de ler e manter
JwJosefy
@JwJosefy Na verdade, o método Task.Delay pode ser implementado usando TaskCompletionSource , da mesma forma que o código acima. A implementação real está aqui: Task.cs
sgnsajgon
4

Isso pode simplificar demais as coisas, mas a fonte TaskCompletion permite aguardar um evento. Como o tcs.SetResult é definido apenas quando o evento ocorre, o chamador pode aguardar a tarefa.

Assista a este vídeo para obter mais informações:

http://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Lucian03-TipsForAsyncThreadsAndDatabinding

nmishr
fonte
1
Coloque aqui o código ou a documentação relevante, pois os links podem mudar com o tempo e tornar essa resposta irrelevante.
precisa saber é o seguinte
3

O cenário do mundo real em que usei TaskCompletionSourceé ao implementar uma fila de downloads. No meu caso, se o usuário iniciar 100 downloads, não quero acioná-los todos de uma vez e, em vez de retornar uma tarefa estratificada, retornarei uma tarefa anexada aTaskCompletionSource . Depois que o download é concluído, o encadeamento que está funcionando, a fila conclui a tarefa.

O principal conceito aqui é que estou me desacoplando quando um cliente solicita que uma tarefa seja iniciada a partir de quando ela realmente é iniciada. Nesse caso, porque não quero que o cliente precise lidar com o gerenciamento de recursos.

observe que você pode usar async / waitit no .net 4, desde que esteja usando um compilador C # 5 (VS 2012+), veja aqui para obter mais detalhes.

Yaur
fonte
0

Eu costumava TaskCompletionSourceexecutar uma tarefa até que ela fosse cancelada. Nesse caso, é um assinante do ServiceBus que normalmente quero executar enquanto o aplicativo for executado.

public async Task RunUntilCancellation(
    CancellationToken cancellationToken,
    Func<Task> onCancel)
{
    var doneReceiving = new TaskCompletionSource<bool>();

    cancellationToken.Register(
        async () =>
        {
            await onCancel();
            doneReceiving.SetResult(true); // Signal to quit message listener
        });

    await doneReceiving.Task.ConfigureAwait(false); // Listen until quit signal is received.
}
Johan Gov
fonte
1
Não há necessidade de usar 'async' com 'TaskCompletionSource', pois ele já criou uma tarefa
Mandeep Janjua
-1

TaskCompletionSourceé para Tarefas como WaitHandleé para Thread. E assim podemos usar TaskCompletionSource para executar sinalização precisa .

Um exemplo é a minha resposta a esta pergunta: Atraso no ContentDialog após clicar em OK

Rhaokiel
fonte