当返回一个Task时,如何正确地链式执行Tasks?

28

在使用C#中的Tasks方面,我感觉还算熟练,但当我尝试从一个方法中返回一个Task并让该方法执行多个任务时,我感到困惑。那么,我应该在我的方法中启动一个新的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);
}

我不确定如何修改这个方法以正确使用 Tasks。我想可能需要在其中加入 .ContinueWith 或者其他的东西。

有可能的解决方案?

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

没有上下文很难做出推荐。您能否提供一些关于返回“Task”的方法的具体细节? - Sergey Vyacheslavovich Brunov
1
你能展示一下 SomeOtherMethodWhichReturnsTaskMethodWhichAlsoReturnsTask 的实际声明吗?现在的情况有点疯狂,因为它返回了一个 Task<Task<string>> 的方法,我怀疑这是错误的... - Reed Copsey
@ Serge 我想这可能是编写一个返回 Task 的方法的最佳方式,当该方法需要调用其他返回 Task 的方法时。我想知道是否需要使用 .ContinueWith 来更改对其他方法的调用。 - Travyguy9
@Travyguy9 如果有正确的方法签名,通常可以在不启动任务的情况下完成此操作。但是如果没有看到实际使用的方法,就无法确定。 - Reed Copsey
1
@ReedCopsey 我不这么认为。我非常确定我使用的UnWrap是在.NET 4.0中的 http://msdn.microsoft.com/en-us/library/dd780917.aspx - Travyguy9
显示剩余3条评论
3个回答

35
一般来说,如果您已经在使用基于任务的方法,则尽可能避免启动新任务。通过链接任务而不是显式阻塞,可以减少系统的开销,因为它不会使线程池线程被绑定等待。
也就是说,通常情况下,像您正在做的那样阻塞会更简单。
请注意,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<Task<string>>转换为:

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

或者,您可以使用 TaskCompletionSource<T> 自行优雅地处理解包:

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

这样做可以在不创建新任务(占用线程池线程)和不阻塞的情况下完成整个过程。
请注意,您可能需要在取消时添加continuations,并在请求取消时使用tcs.SetCancelled

那么我该如何使用链式写法呢?我有一个想法,但感觉不太对。 - Travyguy9
为什么不直接返回 task2 呢?你在那里不需要 TaskCompletionSource - svick
@svick task2 是 Task<Task<string>>,因为第二个方法返回 Task<string> - 我只是编辑了一下以使其更清晰。 - Reed Copsey
GetSomeData may actually block a taskpool thread waiting for GetSomeString to complete, since at the time of t.Result.Result, the t.Result task (which is GetSomeString) would potentially still be ongoing. Instead, this will not block at all: GetSomeInteger(token).ContinueWith(ot => GetSomeString(ot.Result, token).ContinueWith(it => tcs.SetResult(it.Result))); - Ohad Schneider
这里和这里都有关于使用连续任务链接任务的好解释。Unwrap方法的解释也很不错。 - ABS

6

这里有一个我编写的扩展方法来解决这个问题。适用于 .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;
}

5
如果您能提供一个简单的示例,那么使用这个扩展方法会很有帮助 :) - Shiva

0

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


我理解,但当我调用 .Result 时,它应该在那里按顺序运行吗?还是应该将其与 .ContinueWith 链接,一旦获得最终结果,返回该值 (.Result)? - Travyguy9
你可能应该将这些任务链接在一起,但这确实取决于你是独立使用这些任务还是与其他任务在不同的场景中使用。结果肯定应该阻塞 - 这是没有限制的,开发人员应该利用它。使用ContinueWith具有性能优势,因为调度程序将知道要执行哪个线程。 - Dave New

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接