【问题标题】:When should TaskCompletionSource<T> be used?什么时候应该使用 TaskCompletionSource<T>?
【发布时间】:2013-02-25 07:58:54
【问题描述】:

AFAIK,它所知道的是,在某个时候,它的 SetResultSetException 方法被调用以完成通过其 Task 属性公开的 Task&lt;T&gt;

换句话说,它充当Task&lt;TResult&gt; 及其完成的生产者。

我看到here的例子:

如果我需要一种异步执行Func&lt;T&gt; 并拥有Task&lt;T&gt; 的方法 表示该操作。

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假设情况?

【问题讨论】:

  • TaskCompletionSource 主要用于在不创建新线程的情况下将基于事件的异步 api 与 Task 包装起来。

标签: c# .net .net-4.0 task-parallel-library taskcompletionsource


【解决方案1】:

我主要在只有基于事件的 API 可用时使用它 (for example Windows Phone 8 sockets):

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 关键字一起使用时特别有用。

【讨论】:

  • 你能用文字写下我们在这里看到的内容吗?是不是像 SomeApiWrapper 在某处等待,直到发布者引发导致该任务完成的事件?
  • 我的问题 id 没有标记为 c#5,我正在寻找 .net4 的用法。
  • 只是一个更新,微软在 NuGet 上发布了 Microsoft.Bcl.Async 包,它允许在 .NET 4.0 项目中使用 async/await 关键字(建议使用 VS2012 及更高版本)。
  • @Fran_gg7 您可以使用 CancellationToken,请参阅 msdn.microsoft.com/en-us/library/dd997396(v=vs.110).aspx 或作为 stackoverflow 上的新问题
  • 这个实现的问题是这会产生内存泄漏,因为事件永远不会从 obj.Done 释放
【解决方案2】:

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

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

我个人对NetworkStream 类有几个问题,我更喜欢原始的Socket。因为我也喜欢async/await 模式,所以我创建了一个扩展类SocketExtender,它为Socket 创建了几个扩展方法。

所有这些方法都使用TaskCompletionSource&lt;T&gt; 来包装异步调用,如下所示:

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

我将socket 传递给BeginAccept 方法,这样我就可以从编译器中获得轻微的性能提升,而不必提升本地参数。

然后是这一切的美丽:

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

【讨论】:

  • 为什么Task.Factory.StartNew在这里不起作用?
  • @Tola 因为这会创建一个在线程池线程上运行的新任务,但是上面的代码利用了由 BeginAccept 启动的 i/o 完成线程,iow:它不会启动一个新线程。
  • 谢谢,@Frans-Bouma。所以 TaskCompletionSource 是一种将使用 Begin... End... 语句的代码转换为任务的便捷方式?
  • @TolaOdejayi 回复有点晚,但是是的,这是我找到的主要用例之一。它非常适合这种代码转换。
  • 查看TaskFactory<TResult>.FromAsync 以包装Begin.. End... 语句。
【解决方案3】:

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

当您使用缓存时就是一个很好的例子。您可以使用GetResourceAsync 方法,该方法在缓存中查找请求的资源,如果找到资源,则立即返回(不使用新线程,使用TaskCompletionSource)。只有在找不到资源的情况下,我们才会使用新线程并使用Task.Run() 检索它。

可以在此处查看代码示例:How to conditionally run a code asynchonously using tasks

【讨论】:

  • 我确实看到了您的问题以及答案。 (看看我对答案的评论)......:-) 确实这是一个有教育意义的问题和答案。
  • 这其实不是需要TCS的情况。您可以简单地使用Task.FromResult 来执行此操作。当然,如果您使用的是 4.0 并且没有 Task.FromResult,那么您使用 TCS 的目的是编写自己的 FromResult
  • @Servy Task.FromResult 仅从 .NET 4.5 开始可用。在此之前,这是实现这种行为的方式。
  • @AdiLester 你的答案是引用Task.Run,表明它是4.5+。我之前的评论专门针对 .NET 4.0。
  • @Servy 并非每个阅读此答案的人都针对 .NET 4.5+。我相信这是一个很好且有效的答案,可以帮助人们提出 OP 的问题(顺便说一下,它被标记为 .NET-4.0)。无论哪种方式,对我来说,反对它似乎有点过分,但如果你真的相信它值得反对,那就去吧。
【解决方案4】:

this blog post 中,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");

【讨论】:

    【解决方案5】:

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

    我发现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 的这种用法似乎是“不执行代码的任务对象”的另一种情况。

    【讨论】:

      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

        post from the "Parallel Programming with .NET" blog 中有一个真实世界的例子,有一个不错的解释。您确实应该阅读它,但无论如何这里是一个摘要。

        博客文章展示了以下两种实现:

        “一种用于创建“延迟”任务的工厂方法,那些不会 实际上会被安排,直到发生一些用户提供的超时。”

        显示的第一个实现基于Task&lt;&gt;,有两个主要缺陷。第二个实现帖子继续使用TaskCompletionSource&lt;&gt; 来缓解这些问题。

        这是第二个实现:

        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 ,然后在之后使用 action()
        • 因为你回到了你离开的上下文,而 Continuewith 不会保留上下文。 (默认情况下不是)如果 action() 中的下一条语句导致异常,则很难在使用 await 将显示为常规异常的地方捕获它。
        • 为什么不只是await Task.Delay(millisecondsDelay); action(); return; 或(在.Net 4.0 中)return Task.Delay(millisecondsDelay).ContinueWith( _ =&gt; action() );
        • @sgnsajgon 肯定更容易阅读和维护
        • @JwJosefy 其实Task.Delay方法可以通过TaskCompletionSource来实现,和上面的代码类似。真正的实现在这里:Task.cs
        【解决方案8】:

        这可能过于简单了,但 TaskCompletion 源允许人们等待事件。由于 tcs.SetResult 仅在事件发生时设置,调用者可以等待任务。

        观看此视频以获得更多见解:

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

        【讨论】:

        • 请在此处放置相关代码或文档,因为链接可能会随着时间的推移而变化,并使此答案变得无关紧要。
        【解决方案9】:

        我使用TaskCompletionSource 的真实场景是在实现下载队列时。在我的情况下,如果用户开始 100 次下载,我不想一次将它们全部关闭,因此我没有返回一个分层任务,而是返回一个附加到 TaskCompletionSource 的任务。一旦下载完成,队列中的线程就会完成任务。

        这里的关键概念是,当客户要求从实际开始时开始执行任务时,我正在解耦。在这种情况下,因为我不希望客户端必须处理资源管理。

        请注意,只要您使用 C# 5 编译器(VS 2012+),您就可以在 .net 4 中使用 async/await,请参阅 here 了解更多详细信息。

        【讨论】:

          【解决方案10】:

          我使用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.
          }
          

          【讨论】:

          • 不需要使用 'async' 和 'TaskCompletionSource' 因为它已经创建了一个任务
          猜你喜欢
          • 2011-10-14
          • 2010-11-17
          • 2023-04-02
          • 2011-04-15
          • 2017-04-10
          • 2012-03-19
          • 2018-05-12
          • 2018-12-11
          • 1970-01-01
          相关资源
          最近更新 更多