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

时间:2015-07-31 15:28:32

标签: c# .net concurrency task-parallel-library task

I asked a question yesterday,不幸的是,即使提供了答案,我仍然会遇到关于如何正确处理事情的绊脚石...我的问题是我的代码确实有效,但我是一个在并发编程方面是完全新手,感觉我没有正确的编程方式,最重要的是,我害怕养成坏习惯。

为了详细阐述昨天的问题,假设我有以下方法:

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
  • 时使用所有结果

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

就像我说的那样,我的挑战是它实际运行,但我觉得这更像是一种“黑客”正确的编程方式 - 并发编程对我来说是一个新手,我当然希望学习最好的方法应该这样做......

3 个答案:

答案 0 :(得分:3)

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

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

然后我会想到你的代码看起来像这样:

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

答案 1 :(得分:2)

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

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

这有帮助吗?发表评论。

答案 2 :(得分:2)

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

     

我绝对想学习应该做的最佳方法......

首先,区分异步 parallel ,它们是并发的两种不同形式。如果操作不需要线程来完成其工作,则操作是“异步”的良好匹配 - 例如,I / O和其他逻辑操作,其中等待类似计时器的操作。 “并行性”是指在多个内核之间拆分CPU绑定工作 - 如果您的代码以100%最大化一个CPU并且希望使用其他CPU运行得更快,则需要并行性。

接下来,请遵循一些指南,了解在哪些情况下使用哪些API。 Task.Run用于将CPU绑定的工作推送到其他线程。如果您的工作不受CPU限制,则不应使用Task.RunTask<T>.ResultTask.Wait在并行方面更为明显;除了少数例外,它们应该只用于dynamic task parallelism。动态任务并行性非常强大(正如@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在哪些情况下适合的更多信息,我的博客上有Tour of Task,我在my book中介绍了很多并发最佳做法。