ICommand Execute() Task.ContinueWith()

3
在WPF应用程序中实现ICommand类的Execute()方法中,我有一个对外部方法的调用,该方法返回一个Task对象。 ICommand类:
public static void Execute(object parameter)
{
    Cancel(arg1, arg2);
}

private static void Cancel(IList<object> arg1, object arg2)
{

    Task<object> cancelTask = service.AmendAsync
    (
        CancelTokenSource.Token, 
        object arg2
    );

    ProcessCancellingResponse(arg1, arg2);
}

private static void ProcessCancellingResponse(IList<object> arg1, Task<object> cancelTask)
{
    cancelTask.ContinueWith
    (
        task =>
        {
            Update(task.Result.Response);
        },
        CancelTokenSource.Token,
        TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.OnlyOnRanToCompletion,
        TaskScheduler.FromCurrentSynchronizationContext()
    );
}

服务类:

public Task<object> AmendAsync(CancellationToken cancellationToken, object arg1)
{
    return Task<object>.Factory.StartNew
    (
        () =>
        {
            ...
        },
        cancellationToken,
        TaskCreationOptions.None,
        TaskScheduler.Default
    );
}

我的问题是:

  1. 哪个线程调用了ICommend Execute()方法?是UI线程吗?
  2. cancelTask.ContinueWith()会在UI线程还是后台线程上等待?即,如果Task需要很长时间而且正在等待UI线程,那么UI可能会被冻结。

根据Dudi Keleti的回答,我添加了更多的代码,从中我发现底层服务调用实际上会调用Task<object>.Factory.StartNew()。 - Ciaran Martin
3个回答

2
根据Domysee的评论,我在这里澄清,Execute始终在UI线程中运行,但您可以在回调中执行任何操作,包括运行后台线程。
关于继续执行,如果您没有明确告诉它要在哪个线程上继续执行,则会在TaskScheduler.Current上执行其任务,否则,它将在您定义的调度程序上继续执行。
无论如何,考虑使用async\await来进行连续操作,可以选择是否捕获异常。
await Task.Run(() => ).ConfigureAwait(true);

await Task.Run(() => ).ConfigureAwait(false);

更新

根据问题的更新,

执行 -> UI线程

取消 => UI线程

AmendAsync => 后台线程

ContinueWith => UI线程 (因为你编写了 FromCurrentSynchronizationContext)


假设在Execute方法和框架之间没有逻辑(例如RelayCommand或DelegateCommand基类),它不依赖于实现。Execute可以启动一个新的任务,但它将从UI线程调用。 - Domysee
@Domysee 我不会做任何假设;) 你说得对,但它仍然可以出现在任何线程中,这取决于实现方式。 - Dudi Keleti
1
谢谢你的简明答案。可以说,我本应该通过适当的调试自己来解决这个问题,但我认为不得不提出问题并获得答案帮助我解决了它。因为我的代码中的AmendAsync在后台线程上完成实际工作,UI交互发生在UI线程上,所以这不是一个问题。 - Ciaran Martin

2

什么线程调用ICommend Execute()是UI线程吗?

是的,它总是在UI线程上。

cancelTask.ContinueWith()会在UI线程还是后台线程上等待?

ContinueWith只是一个普通的方法调用。没有任何魔法。所以它可以分解为:

这个:

private static void ProcessCancellingResponse(IList<object> arg1, Task<object> cancelTask)
{
  cancelTask.ContinueWith
  (
    task =>
    {
        Update(task.Result.Response);
    },
    CancelTokenSource.Token,
    TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.OnlyOnRanToCompletion,
    TaskScheduler.FromCurrentSynchronizationContext()
  );
}

这意味着:

与此相同:

private static void ProcessCancellingResponse(IList<object> arg1, Task<object> cancelTask)
{
  Action<Task> continuation = task => { Update(Task.Result.Response); };
  var token = CancelTokenSource.Token;
  var options = TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.OnlyOnRanToCompletion;
  var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
  cancelTask.ContinueWith(continuation, token, options, scheduler);
}

由于在UI线程上调用了ProcessCancellingResponse,所以scheduler将是一个在UI线程上执行其任务的调度器。因此,continuation将在UI线程上运行。
另外一件事,我发现至少有一个错误:AttachedToParent几乎肯定是错误的。承诺任务(异步任务)几乎永远不应该是附加任务。
实现也可以更加简洁:
private static async Task ProcessCancellingResponseAsync(IList<object> arg1, Task<object> cancelTask)
{
  var result = await cancelTask;
  Update(result.Response);
}

public object Amend(CancellationToken cancellationToken, object arg1)
{
  ...
}

private static void Cancel(IList<object> arg1, object arg2)
{
  Task<object> cancelTask = Task.Run(() => service.Amend
  (
    CancelTokenSource.Token, 
    object arg2
  );

  ProcessCancellingResponse(arg1, arg2);
}

请问你能详细解释一下为什么不应该将continuations设置为AttachedToParent吗? - Ciaran Martin
AttachedToParent 改变了前置任务的语义。对于 Promise 任务,这会改变任务的含义。例如,“下载”任务被更改为“下载和处理任务”。这对于上游代码来说是令人惊讶的行为,因此用于异步代码的任务通常指定 DenyChildAttach(覆盖 AttachedToParent)。 - Stephen Cleary
ContinueWith 不是因为从 Execute 调用它而与 Cancel 相关联吗?延续从调用它的方法开始,而不是从返回任务的异步方法开始。因此,“AttachedToParent” 是有意义的。我有什么遗漏的吗? - Ciaran Martin
我之前无法编辑上一条评论,请忽略它。请阅读这个。抱歉,现在我正在手机上回答。ContinueWith 不是通过 Cancel 调用的吗?而 Cancel 又是由 Execute 调用的,这些都在 UI 线程上执行了吗?继续操作是从调用它的方法而不是返回任务的异步方法开始的。因此,就我所知,AttachedToParent 是有意义的。调用和继续应该在同一个线程上。唯一在另一个线程上的是异步任务。我有什么遗漏的吗? - Ciaran Martin
AttachedToParent 与其执行的线程无关。更多信息请参见此处 - Stephen Cleary

1
  1. 当直接从UI调用ICommand Execute()时,它会在主线程(UI线程)上运行。

  2. 这取决于代码所在的位置。如果它直接位于Execute内部,则也会在主线程上运行,因为您正在指定调度程序为 TaskScheduler.FromCurrentSynchronizationContext()


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