使用 Task<T> 进行非实时任务

3

我有一个 Operation 类,像这样...

public sealed class Operation
{
  public void DoSomething1(ArgType1 arg) {...}
  public void DoSomething2(ArgType2 arg) {...}
  ...
  public Task<bool> Execute() {...}
}

DoSomething方法打包需要完成的工作,存储参数并且Execute()方法将启动一个Task以原子方式完成这项工作。 DoSomething的主要影响是副作用,但其中一些也有意义返回值,我的第一反应是返回一个Task,如下所示...

  public Task<ResultType3> DoSomething3(ArgType3 arg) {...}

但问题是,这个Task不像大多数任务一样是“实时”的。在调用Execute()以启动工作之前,等待该Task的结果将是没有意义的,因此我认为这会让消费者感到困惑。好像DoSomething3()Execute()的返回值是相互依赖的任务。

我可以将Task<>包装在一个新类型中,称为Result<>,并在内部它将保存一个Task<>,而Operation将保留其TaskCompletionSource<>并在Execute()结束时设置Result,以便客户端在等待由Execute()返回的Task后可以观察到Result

public Result<T>
{ 
  internal Result(Task t) { _t = t; }
  public bool IsComplete { get { return _t.IsComplete; } }
  public T Result { get { return _t.Result; } }
  // Perhaps more methods delegating to the underlying Task
}

  public Result<ResultType4> DoSomething4(ArgType4 arg) {...}

包装Task的主要动机是向消费者传达DoSomething3()的结果不是实时Task,并使其难以/不可能调用...

var result = await op.DoSomething4(x);

由于没有人启动操作,因此这可能会导致代码死锁。请注意,这个Result<>类型与不同语义的Nullable<>相似。

另一种方法是让该方法返回某些不透明对象,该对象将用作从Execute()完成后检索实际结果的键...

var token = op.DoSomething4(x);
...
var succeeded = await op.Execute();
if (! succeeded) return;
var result = op.RetrieveResult(token);

“Retrieve result” 的签名类似于……
public T RetrieveResult(Token<T> token) {...}

我认为另一个选择是添加一个额外的参数作为回调函数,在 Execute() 结束时执行,当实际结果可用时...

public void DoSomething5(ArcType5 arg, Func<ResultType5,Task> callback) {...}

正如您所看到的,我有几个不同的选项,但是并没有一个强烈的直觉告诉我哪个最合适。不幸的是,这可能主要是品味问题,但我希望能够得到有关不同方法的反馈。


DoSomethingN返回东西是绝对必要的吗?例如,您不能返回一个由Execute(立即)返回的Result类,该类具有与每个DoSomethingN的结果相对应的Task成员吗?然后,您的消费代码可能如下所示:op.EnqueueWork1(someDataForWork1); op.EnqueueWork1(someDataForWork2); var result = op.Execute(); var work2Result = await result.Work2Results; - Asad Saeeduddin
只是为了明确起见,我所说的“Result”并不基于你在问题中描述的内容,它只是拥有相同的名称。 - Asad Saeeduddin
感谢您的回复。操作是一组原子,它们将被捆绑并作为一个整体发送以原子方式执行(通常进行数据库写入)。主要返回结果实际上是新创建行的键(我们可以称之为DoSomething、Create或者像Asad建议的EnqueueCreate)。当然,还可以添加其他操作,例如修改和删除以及更高级别的操作,并且可以在操作中多次调用Create以使用不同的参数生成多个行,消费者需要将结果与操作匹配。 - John Jones
@JohnJones 所以,再次重申我的问题,从你的 DoWhatever 方法中返回值是否绝对必要?你可以将它们全部设置为空(void),并让 Execute 返回一个类型实例,该实例公开表示一些排队工作对应的每个子结果的 Task<SomeData>,以及用于整体完成的 Task<bool> - Asad Saeeduddin
根据反馈,我决定让方法返回void并接受一个可选的lambda(Action<ResultType>)来捕获结果,这样每个操作的每个部分都保持独立(将所有这些基本独立但类型不同的结果分组到单个包中有点人为)。 - John Jones
显示剩余3条评论
1个回答

2
我找不到你拥有不同方法的原因,这些方法只设置值(而不是属性),还有一个单独的方法运行所有内容。
但是,如果你想保留这种设计,你可以采用与TPL Dataflow的块非常相似的方式。拥有一个“Completion”任务属性,仅在“Execute”完成时才完成,并且让“DoSomething3”为空。这使用户可以理解可以等待整个操作(包括“Execute”)而不仅仅是“DoSomething3”。
public sealed class Operation
{
    private TaskCompletionSource<bool> _tcs
    public Task Completion {get { return _tcs.Task;} }
    public void DoSomething3(ArgType2 arg) {...}
    ...
    public Task<bool> Execute() 
    {
        // ...
        _tcs.SetResult(false);
    }
}

使用方法:

operation.DoSomething3(arg);
await operation.Completion;

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