【问题标题】:What is the proper way to chain Tasks when returning a Task?返回任务时链接任务的正确方法是什么?
【发布时间】:2014-08-24 11:09:51
【问题描述】:

我非常喜欢在 C# 中使用 Tasks,但是当我尝试从方法返回 Task 并且该方法将在其内部执行多个任务时,我感到困惑。那么我是否让我的方法启动一个新任务,然后在其中按顺序执行所有操作?用 .ContinueWith() 完成这一切很难让我全神贯注

例子:

public Task<string> GetSomeData(CancellationToken token)
{
    return Task.Factory.StartNew(() =>
    {
        token.ThrowIfCancellationRequested();

        var initialData = GetSomeInteger(token).Result;

        return GetSomeString(initialData, token).Result;
    });
}

public Task<int> GetSomeInteger(CancellationToken token)
{
    return Task<int>.Factory.StartNew(() =>
    {
        return 4;
    }, token);
}

public Task<string> GetSomeString(int value, CancellationToken token)
{
    return Task<string>.Factory.StartNew(() =>
    {
        return value.ToString();
    }, token);
}

我不确定如何编写此方法以使其正确使用任务。我想我只是觉得那里应该有一个 .ContinueWith 之类的东西。

可能的修复??

public Task<string> GetSomeData(CancellationToken token)
{
    return GetSomeInteger(token).ContinueWith((prevTask) =>
    {
        return GetSomeString(prevTask.Result, token);
    }, token).Unwrap();
}

【问题讨论】:

  • 如果没有上下文,很难推荐一些东西。您能否提供一些有关返回 Tasks 的方法的具体细节?
  • 你能显示SomeOtherMethodWhichReturnsTask和MethodWhichAlsoReturnsTask的实际声明吗?你现在拥有的有点疯狂,因为它是一个返回 Task&lt;Task&lt;string&gt;&gt; 的方法,我怀疑这是错误的......
  • @Serge 我猜这是否是编写返回任务的方法的最佳方法,当该方法需要调用其他也返回任务的方法时。我想知道是否需要使用 .ContinueWith 将这些调用更改为其他方法
  • @ReedCopsey 确定我刚刚写了这些并编造了一些东西。认为不需要知道那些其他方法是什么或做了什么。
  • @ReedCopsey 我不这么认为。我很确定我使用的 UnWrap 在 .NET 4.0 msdn.microsoft.com/en-us/library/dd780917.aspx

标签: c# task-parallel-library


【解决方案1】:

一般来说,如果您已经在使用基于任务的方法,最好尽量避免启动新任务。链接任务而不是显式阻塞将减少系统的开销,因为它不会让 ThreadPool 线程处于等待状态。

话虽如此,在你正在做的时候阻止通常更简单。

请注意,C# 5 使这变得更加简单,它提供了一个 API,可以为您提供两全其美的功能:

public async Task<string> GetSomeData(CancellationToken token)
{
    token.ThrowIfCancellationRequested();

    var initialData = await SomeOtherMethodWhichReturnsTask(token);

    string result = await initialData.MethodWhichAlsoReturnsTask(token);

    return result;
};

更新后编辑:

鉴于新代码,没有一种简单的方法可以直接与ContinueWith 链接。有几个选项。您可以使用Unwrap 转换您创建的Task&lt;Task&lt;string&gt;&gt;,即:

public Task<string> GetSomeData(CancellationToken token)
{
    Task<Task<string>> task = GetSomeInteger(token)
                               .ContinueWith(t => 
                               {
                                   return GetSomeString(t.Result, token);
                               }, token);
    return task.Unwrap();
}

或者,您可以使用TaskCompletionSource&lt;T&gt; 优雅地处理自己的解包:

public Task<string> GetSomeData(CancellationToken token)
{
    var tcs = new TaskCompletionSource<string>();

    Task<int> task1 = GetSomeInteger(token);
    Task<Task<string>> task2 = task1.ContinueWith(t => GetSomeString(t.Result, token));
    task2.ContinueWith(t => tcs.SetResult(t.Result.Result));
    return tcs.Task;
}

这允许整个进程在不创建新任务(占用线程池线程)的情况下工作,并且不会阻塞。

请注意,您可能希望在取消时添加延续,并在请求取消时使用tcs.SetCancelled。

【讨论】:

  • 那我该如何用链式编写呢?我有一个想法,但感觉不对
  • 为什么不直接返回task2?你不需要那里的TaskCompletionSource。
  • @svick task2 是 Task&lt;Task&lt;string&gt;&gt;,因为第二种方法返回 Task&lt;string&gt; - 我只是进行了编辑以使其更清晰。
  • GetSomeData 实际上可能会阻塞等待GetSomeString 完成的任务池线程,因为在t.Result.Result 时,t.Result 任务(即GetSomeString)可能仍然存在进行中。相反,这根本不会阻塞:GetSomeInteger(token).ContinueWith(ot =&gt; GetSomeString(ot.Result, token).ContinueWith(it =&gt; tcs.SetResult(it.Result)));
  • Here 和 here 很好地解释了 Unwrap 方法
【解决方案2】:

这是我为解决此问题而构建的扩展方法。适用于 .Net 4+

public static Task<TNewResult> ContinueWith<T, TNewResult>(this Task<T> task, Func<Task<T>, Task<TNewResult>> continuationFunction, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<TNewResult>();
    task.ContinueWith(t => 
    {
        if (cancellationToken.IsCancellationRequested)
        {
            tcs.SetCanceled();
        }
        continuationFunction(t).ContinueWith(t2 => 
        {
            if (cancellationToken.IsCancellationRequested || t2.IsCanceled)
            {
                tcs.TrySetCanceled();
            }
            else if (t2.IsFaulted)
            {
                tcs.TrySetException(t2.Exception);
            }
            else
            {
                tcs.TrySetResult(t2.Result);
            }
        });
    });
    return tcs.Task;
}

【讨论】:

  • 如果您还可以通过一个简单的示例发布此扩展方法的用法,将会有所帮助:)
【解决方案3】:

是的,一切都将在您的主要任务中按顺序运行。这是因为调用 Result 属性会阻塞当前线程,直到返回值。

【讨论】:

  • 我明白,但是当我调用 .Result 时,它应该在那里按顺序运行吗?或者我应该用 .ContinueWith 链接它,一旦我得到最终结果,返回那个值 (.Result)
  • 您可能应该将这些任务链接在一起,但这确实取决于您是独立使用这些任务还是在其他一些不同场景中与其他任务一起使用。结果肯定应该阻塞 - 这是没有限制的,开发人员应该利用它。使用 ContinueWith 具有性能优势,因为调度程序将知道执行它的线程。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多