当在不同时间需要返回值时,将任务链接在一起的正确方法#2



我昨天问了一个问题,不幸的是,即使有了提供的答案,我仍然在如何正确地做事情的绊脚石…我的问题是,我的代码实际上可以工作,但我是并发编程的完全新手,感觉我没有以正确的方式编程,最重要的是,我害怕养成坏习惯。

举一个简单的例子来阐述昨天的问题,假设我有以下方法:

static Task<IEnumerable<MyClass>> Task1(CancellationToken ct)
static Task<IEnumerable<int>> Task2(CancellationToken ct, List<string> StringList)
static Task<IEnumerable<String>> Task3(CancellationToken ct)
static Task<IEnumerable<Double>> Task4(CancellationToken ct)
static Task Task5(CancellationToken ct, IEnumerable<int> Task2Info, IEnumerable<string> Task3Info, IEnumerable<double> Task4Info)
static Task Task6(CancellationToken ct, IEnumerable<int> Task2Info, IEnumerable<MyClass> Task1Info)

我编写的使用它们的代码如下:

static Task execute(CancellationToken ct)
{
    IEnumerable<MyClass> Task1Info = null;
    List<string> StringList = null;
    IEnumerable<int> Task2Info = null;
    IEnumerable<string> Task3Info = null;
    IEnumerable<double> Task4Info = null;
    var TaskN = Task.Run(() =>
                    {
                        Task1Info = Task1(ct).Result;
                    }
                    , ct)
                    .ContinueWith(res =>
                    {
                        StringList = Task1Info.Select(k=> k.StringVal).ToList();
                        Task2Info = Task2(ct, StringList).Result;
                    }
                    , ct);
    return Task.Run(() =>
    {
        return Task.WhenAll
            (
                TaskN,
                Task.Run(() => { Task3Info = Task3(ct).Result; }, ct),
                Task.Run(() => { Task4Info = Task4(ct).Result; }, ct)
            )
            .ContinueWith(res =>
            {
                Task5(ct, Task2Info, Task3Info, Task4Info).Wait();
            }
            , ct)
            .ContinueWith(res =>
            {
                Task6(ct, Task2Info, Task1Info).Wait();
            }
            , ct);
    });
}

也就是说:

  • 我需要Task1的结果来计算StringList并运行Task2
  • Task2Task3Task4可以同时运行
  • 我需要上述所有方法的返回值用于以后的方法调用
  • 一旦这些运行,我使用它们的结果运行Task5
  • 一旦Task5运行,我使用运行Task6的所有结果

作为一个简单的解释,假设第一部分是数据收集,第二部分是数据清理,第三部分是数据报告

就像我说的,我的挑战是这实际上是运行的,但我只是觉得它更像是一个"hack",而不是正确的编程方式——并发编程对我来说是非常新的,我当然想学习最好的方法来完成它…

如果我能看到你的TaskN方法是如何实现的,我会对我的答案感觉更好。具体来说,我想验证是否需要将TaskN方法调用包装在对Task.Run()的调用中,如果它们已经返回Task返回值。

但就我个人而言,我只会使用async-await风格的编程。我发现它相当容易读/写。文档可以在这里找到。

我会设想你的代码看起来像这样:

static async Task execute(CancellationToken ct)
{
    // execute and wait for task1 to complete.
    IEnumerable<MyClass> Task1Info = await Task1(ct);
    List<string> StringList = Task1Info.Select(k=> k.StringVal).ToList();
    // start tasks 2 through 4
    Task<IEnumerable<int>> t2 = Task2(ct, StringList);
    Task<IEnumerable<string>> t3 = Task3(ct);
    Task<IEnmerable<Double>> t4 = Task4(ct);
    // now that tasks 2 to 4 have been started,
    // wait for all 3 of them to complete before continuing.
    IEnumerable<int> Task2Info = await t2;
    IEnumerable<string> Task3Info = await t3;
    IEnumerable<Double> Task4Info = await t4;
    // execute and wait for task 5 to complete
    await Task5(ct, Task2Info, Task3Info, Task4Info);
    // finally, execute and wait for task 6 to complete
    await Task6(ct, Task2Info, Task1Info);
}

我不完全理解问题中的代码,但是通过依赖关系链接任务非常容易。例子:

var t1 = ...;
var t2 = ...;
var t3 = Task.WhenAll(t1, t2).ContinueWith(_ => RunT3(t1.Result, t2.Result));

你可以像这样表达任意的依赖DAG。这也使得不需要存储到局部变量中,尽管如果需要的话可以这样做。

这样你也可以摆脱尴尬的Task.Run(() => { Task3Info = Task3(ct).Result; }, ct)模式。这与Task3(ct)相同,除了存储到局部变量,而局部变量可能一开始就不应该存在。

.ContinueWith(res =>
        {
            Task5(ct, Task2Info, Task3Info, Task4Info).Wait();
        }

这可能应该是

.ContinueWith(_ => Task5(ct, Task2Info, Task3Info, Task4Info)).Unwrap()

有帮助吗?请留言

感觉我没有以正确的方式编程,最重要的是,我害怕养成坏习惯。

我很想学习做这件事的最好方法…

首先,区分异步并行,它们是并发的两种不同形式。一个操作可以很好地匹配"异步"操作。如果它不需要线程来完成它的工作-例如,I/O和其他逻辑操作,您等待定时器之类的东西。"Parallelism"是关于在多个核心上分割CPU限制的工作-如果你的代码在一个CPU上达到100%的最大利用率,并且你希望它通过使用其他CPU来运行得更快,你需要并行性。

接下来,遵循一些指导原则,哪些api在哪些情况下使用。Task.Run用于将cpu绑定的工作推送到其他线程。如果您的工作不受cpu限制,则不应该使用Task.RunTask<T>.ResultTask.Wait更偏向于平行;除了少数例外,它们实际上应该只用于动态任务并行。动态任务并行性非常强大(正如@usr所指出的,您可以表示任何DAG),但它也很低级,使用起来很笨拙。几乎总是有更好的方法。ContinueWith是另一个动态任务并行API的例子。

因为你的方法签名返回Task,我将假设它们是自然异步的(意思是:它们不使用Task.Run,并且可能是用I/O实现的)。在这种情况下,Task.Run是不正确的工具。适合异步工作的工具是asyncawait关键字。

所以,一次一个地采取你想要的行为:

// I need the results of Task1 to calculate StringList and to run Task2
var task1Result = await Task1(ct);
var stringList = CalculateStringList(task1Result);
// Task2, Task3 and Task4 can all run concurrently
var task2 = Task2(ct, stringList);
var task3 = Task3(ct);
var task4 = Task4(ct);
await Task.WhenAll(task2, task3, task4);
// I need the return values from all of the above for later method calls
var task2Result = await task2;
var task3Result = await task3;
var task4Result = await task4;
// Once these are run, I use their results to run Task5
await Task5(ct, task2Result, task3Result, task4Result);
// Once Task5 is run, I use all the results in running Task6
await Task6(ct, task2Result, task1Result);

关于什么api适合在什么情况下使用的更多信息,我在我的博客上有一个任务指南,我在我的书中介绍了许多并发性最佳实践。

最新更新