什么时候应该使用TaskCompletionSource <T>?


199

AFAIK,它所知道的只是在某个时候,它的SetResultSetException方法被调用以Task<T>通过其Task属性完成公开。

换句话说,它充当a Task<TResult>及其完成的生产者。

我在这里看到了示例:

如果我需要一种异步执行Func并有一个Task来表示该操作的方法。

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; 
}

*如果我没有,可以使用Task.Factory.StartNew-但我确实Task.Factory.StartNew

题:

可有人请举例相关的情景解释直接TaskCompletionSource 而不是一个假想中,我没有的情况 Task.Factory.StartNew


5
TaskCompletionSource主要用于使用Task包装基于事件的异步api,而无需创建新线程。
阿维斯(Arvis)2015年

Answers:


230

当仅基于事件的API可用时(例如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;
}

因此,与C#5 async关键字一起使用时,它特别有用。


4
你能用文字写我们在这里看到什么吗?就像SomeApiWrapper是在某个地方等待,直到发布者引发导致该任务完成的事件一样?
罗伊·纳米尔

看一下我刚刚添加的链接
GameScripting 2013年

6
只是更新,Microsoft已Microsoft.Bcl.Async在NuGet 上发布了该软件包,该软件包允许async/await.NET 4.0项目中的关键字(建议使用VS2012及更高版本)。
Erik

1
@ Fran_gg7您可以使用CancellationToken,请参阅msdn.microsoft.com/zh-cn/library/dd997396(v=vs.110).aspx,也可以在此处作为stackoverflow上的新问题
GameScripting

1
此实现的问题是,由于事件从未从obj.Done中释放,因此这会产生内存泄漏。
Walter Vehoeven

78

以我的经验,TaskCompletionSource非常适合将旧的异步模式包装到现代async/await模式。

我能想到的最有益的例子是使用Socket。它具有旧的APM和EAP模式,但没有和具有的awaitable Task方法。TcpListenerTcpClient

我个人在NetworkStream班上有几个问题,更喜欢原始课程Socket。由于我也喜欢该async/await模式,因此我SocketExtender创建了一个扩展类,它为创建了几个扩展方法Socket

所有这些方法都利用TaskCompletionSource<T>来包装异步调用,如下所示:

    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;
    }

我传递socketBeginAccept方法,使我获得稍许的性能提升出来的没有扯起本地参数的编译器。

那么这一切的美丽:

 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();

1
为什么Task.Factory.StartNew在这里不起作用?
Tola Odejayi 2014年

23
@Tola那样会创建一个在线程池线程上运行的新任务,但是上面的代码利用了BeginAccept启动的I / O完成线程,ow:它不会启动新线程。
弗朗斯·布玛

4
谢谢,@ Frans-Bouma。那么TaskCompletionSource是将使用Begin ... End ...语句转换为任务的代码的便捷方法吗?
Tola Odejayi 2014年

3
@TolaOdejayi回复很晚,但是是的,这是我发现的主要用例之一。对于代码的这种过渡,它非常有用。
Erik

4
查看TaskFactory <TResult> .FromAsync来包装Begin.. End...语句。
MicBig

37

对我来说,一个典型的使用场景TaskCompletionSource是,当我的方法不一定必须执行耗时的操作时。它允许我们做的是选择要使用新线程的特定情况。

一个很好的例子是使用缓存。您可以有一个GetResourceAsync方法,该方法在高速缓存中查找所请求的资源,并在找到资源后立即返回(不使用,使用新线程TaskCompletionSource)。仅在找不到资源的情况下,我们才想使用一个新线程并使用进行检索Task.Run()

在此处可以看到一个代码示例:如何使用任务有条件地异步运行代码


我确实看到了您的问题和答案。(请看我对答案的评论).... :-),确实这是一个教育性的问答。
罗伊·纳米尔

11
实际上,这不是需要TCS的情况。您可以简单地使用Task.FromResult它。当然,如果您使用的是4.0,而没有Task.FromResult使用TCS,那您需要编写自己 的TCS FromResult
Servy 2014年

@Servy Task.FromResult仅从.NET 4.5开始可用。在此之前,这就是实现此行为的方法。
Adi Lester 2014年

@AdiLester您的答案是引用Task.Run,表明它是4.5+。我之前的评论专门针对.NET 4.0。
Servy 2014年

@Servy并非所有人都知道此答案的目标是.NET 4.5+。我认为这是一个很好且有效的答案,可以帮助人们提出OP的问题(顺便标记为.NET-4.0)。无论哪种方式,对我而言,降低投票似乎都有些许困难,但如果您真的认为值得投票,那就继续吧。
Adi Lester 2014年

25

本博客中,Levi Botelho描述了如何使用TaskCompletionSource来为Process编写异步包装,以便您可以启动它并等待其终止。

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;
}

及其用法

await RunProcessAsync("myexecutable.exe");

14

似乎没有人提及,但是我想单元测试也可以被认为是现实生活

我发现TaskCompletionSource在使用异步方法模拟依赖项时很有用。

在实际的测试程序中:

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

在单元测试中:

// 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);
  // ...
}

毕竟,TaskCompletionSource的这种用法似乎是“不执行代码的Task对象”的另一种情况。


11

TaskCompletionSource用于创建不执行代码的Task对象。在现实世界中,TaskCompletionSource是I / O绑定操作的理想选择。这样,您将获得任务的所有好处(例如,返回值,连续性等),而不会在操作期间阻塞线程。如果您的“函数”是受I / O约束的操作,则不建议使用新Task阻塞线程。而是使用TaskCompletionSource,您可以创建一个从属任务,以仅指示您的I / O绑定操作何时完成或出现故障。


5

这篇来自“ .NET的并行编程”博客的文章中有一个真实的例子,并给出了不错的解释。您确实应该阅读它,但是无论如何这是一个摘要。

博客文章显示了两种实现:

“一种用于创建“延迟”任务的工厂方法,这些任务实际上不会在用户提供的某些超时发生之前进行调度。”

所示的第一个实现基于,Task<>并且有两个主要缺陷。第二个实现文章继续通过使用减轻这些影响TaskCompletionSource<>

这是第二个实现:

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();
    });
}

最好在tcs.Task上使用await,然后在
Royi Namir 2014年

5
beucase会返回到您离开的上下文,而Continuewith不会保留上下文。(默认情况下不是)(如果action()中的下一条语句引起异常),也很难在使用await将您显示为常规异常的地方捕获它。
罗伊·纳米尔

3
为什么不只是await Task.Delay(millisecondsDelay); action(); return;或(在.Net 4.0中)return Task.Delay(millisecondsDelay).ContinueWith( _ => action() );
sgnsajgon

@sgnsajgon肯定会更易于阅读和维护
JwJosefy 2014年

@JwJosefy实际上,可以使用TaskCompletionSource来实现Task.Delay方法,类似于上面的代码。真正的实现在这里:Task.cs
sgnsajgon


3

我使用的现实世界场景TaskCompletionSource是实现下载队列时。在我的情况下,如果用户开始进行100次下载,则我不想立即将其全部解雇,因此,我不返回分散的任务,而是返回附加到的任务TaskCompletionSource。下载完成后,正在使用队列的线程将完成任务。

这里的关键概念是,当客户要求从实际启动任务开始时,我就将其解耦了。在这种情况下,因为我不希望客户端不得不处理资源管理。

请注意,只要您使用的是C#5编译器(VS 2012+),就可以在.net 4中使用async / await 。有关更多详细信息,请参见此处


0

我曾经TaskCompletionSource运行过一个任务,直到它被取消。在这种情况下,我通常要在应用程序运行期间一直运行ServiceBus订户。

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.
}

1
无需将'async'与'TaskCompletionSource'一起使用,因为它已经创建了任务
Mandeep Janjua

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.