如何将任务结果传递给不使用延续的其他任务

Posted

技术标签:

【中文标题】如何将任务结果传递给不使用延续的其他任务【英文标题】:How to pass Task results to other Tasks not using continuations 【发布时间】:2017-03-08 23:48:22 【问题描述】:

我在Func<Task>types 中定义了可变数量的任务,因此我可以定义任务但不能让它们自动启动。比如说:

Func<Task> task1Func = () => DoTask1Async();

以这种方式定义的一些任务返回值。我需要一种方法将此类任务的结果“传递”给以相同方式定义的后续任务的参数。所以,我想写:

Func<Task> task2Func = () => DoTask2Async(task1Func.Invoke().Result));

这样做的问题是task1Func.Invoke() 部分再次启动task1 任务,这是不好的。

我无法将输入重新构建为链延续,例如:

Func<Task> funcTasks = () => DoTask1Async().ContinueWith(t => DoTask2Async(t.Result));

因为我需要在代码中知道链中每个任务(即 DoTask1Async / DoTask2Async)完成运行其他代码的点。另一个问题是我不知道如何计算“链”中涉及多少任务。

我无法在任务结果完成时存储它们,因为任务需要像 task1Functask2Func 这样以声明方式定义。

任务按定义的顺序处理,每个任务在处理下一个任务之前完成。任务的结果仅用于后续任务。

编辑

响应使用task1Func.Invoke().Result 的可运行代码的请求。创建一个新的 WPF 项目并向默认 Grid 添加一个按钮。然后清除 MainWindow.xaml.cs 文件并粘贴以下内容。这是最接近我真实项目的代码,同时删除了所有与问题无关的内容。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;

namespace WpfApplication2


/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window

    public MainWindow()
    
        InitializeComponent();
    

    private async void Button_Click(object sender, RoutedEventArgs e)
    
        // Declare first task that returns a result
        Func<Task<int>> deferredTask1 = () => DoTask1Async();
        // Declare second task using result of first.
        // BUT this isn't good as it calls deferredTask1 AGAIN, so need another approach.
        Func<Task> deferredTask2 = () => DoTask2Async(deferredTask1.Invoke().Result);

        var item1 = new MyItem()  MyDeferredTask = deferredTask1 ;
        var item2 = new MyItem()  MyDeferredTask = deferredTask2 ;

        List<MyItem> items = new List<MyItem>();

        items.Add(item1);
        items.Add(item2);

        await DoTasksAsync(items);

    

    public static async Task<int> DoTask1Async()
    
        return await Task<int>.Run(() =>  return 3000; );
    

    public static async Task DoTask2Async(int delay)
    
        await Task.Delay(delay);
    

    private async Task DoTasksAsync(IEnumerable<MyItem> items)
    
        foreach (var item in items)
        
            await item.MyDeferredTask();
        
    

    private class MyItem
    
        public Func<Task> MyDeferredTask  get; set; 
    


【问题讨论】:

你能把签名从Func&lt;Task&gt;改成Func&lt;Task,Task&gt;吗? @ScottChamberlain,是的,这是可能的。 我认为我们需要一个更完整的示例来说明如何设置多个任务并将它们链接起来。使用示例中的task1Func.Invoke().Result 版本为我们创建runnable example。它将极大地帮助我们向我们展示如何以更好的方式做到这一点。 @ScottChamberlain ,请参阅我上面的编辑以响应您的请求。希望这会有所帮助。 你确定你是从正确的方向来的吗?像Dataflow 这样的东西可能是一个更好的起点吗? 【参考方案1】:

使用 Microsoft 的响应式框架,您可以做到这一点:

var query =
    from r1 in Observable.FromAsync(() => DoTask1Async())
    from r2 in Observable.FromAsync(() => DoTask2Async(r1))
    select new  r1, r2 ;

await query;

【讨论】:

【参考方案2】:

您可以将这些延迟的异步操作表示为嵌套任务 (Task&lt;Task&gt;)。外层任务代表操作的开始,内层任务代表操作的完成。内层任务由unwrapping外层任务获取:

Task<Task<int>> task1 = new Task<Task<int>>(async () =>

    return await DoTask1Async();
);

Task<Task> task2 = new Task<Task>(async () =>

    try  task1.RunSynchronously();  catch (InvalidOperationException)  
    int task1Result = await task1.Unwrap();
    await DoTask2Async(task1Result);
);

外部任务开始它的生命是冷的。您必须调用其方法StartRunSynchronously 才能变热并开始运行。这两个方法只能调用一次。他们第二次抛出InvalidOperationException,所以为了保持在不同时间从多个位置启动任务的灵活性,你必须预料到这个异常并吞下它。

为了将这些操作存储在List 中,您必须分别存储外部任务和内部任务,因为外部任务是您开始的,内部任务是您等待的。把它们放在tuplesValueTuple&lt;Task, Task&gt; 里很方便:

List<(Task, Task)> tasks = new List<(Task, Task)>()

    (task1, task1.Unwrap()),
    (task2, task2.Unwrap()),
;

foreach (var (outerTask, innerTask) in tasks)

    try  outerTask.RunSynchronously();  catch (InvalidOperationException)  
    await innerTask;

这是允许的,因为泛型类 Task&lt;TResult&gt; 派生自基类 Task


替代方案: 不使用嵌套的Task&lt;Task&gt;s,另一种选择是使用Lazy&lt;Task&gt;s。 Lazy 类保证被包装的任务只会被实例化一次,并且具有线程安全性。不过有一个困难。我们不能将Lazy&lt;Task&gt;Lazy&lt;Task&lt;T&gt;&gt; 的实例放在同一个List 中。 C# 编译器认为它们是完全不同的类。因此,让我们制作两个 LazyTask 包装器,通用的包装器派生自非通用包装器,模仿 Task&lt;T&gt;Task 之间的关系:

class LazyTask

    private readonly Lazy<Task> _lazyTask;

    public LazyTask(Func<Task> function) => _lazyTask = new Lazy<Task>(function);

    public Task Task => _lazyTask.Value;

    public TaskAwaiter GetAwaiter() => this.Task.GetAwaiter(); // Make it awaitable


class LazyTask<T> : LazyTask

    public LazyTask(Func<Task<T>> function) : base(function)  

    public new Task<T> Task => (Task<T>)base.Task;

    public T Result => this.Task.GetAwaiter().GetResult();

    public new TaskAwaiter<T> GetAwaiter() => this.Task.GetAwaiter();

有了这些类,实现变得比“嵌套任务”方法更简洁。无需手动启动任务并吞下异常。任务会根据需要自动创建。

LazyTask<int> task1 = new LazyTask<int>(async () =>

    return await DoTask1Async();
);

LazyTask task2 = new LazyTask(async () =>

    await DoTask2Async(await task1);
);

List<LazyTask> tasks = new List<LazyTask>()  task1, task2 ;

foreach (var task in tasks)

    await task;

【讨论】:

以上是关于如何将任务结果传递给不使用延续的其他任务的主要内容,如果未能解决你的问题,请参考以下文章

C# .NET 4,如何将任务完成结果传递给另一个方法?

使用 django 表单提交传递变量

使用celery的backend异步获取结果

将异步结果从父组件传递给子组件反应功能组件

如何在子任务上使用延续返回 Task<Object>

如何将 NSURLRequest 的结果传递回包含类 [重复]