使用异步代替Task.Run()

3
我有以下代码片段:
private void btnAction_Click(object sender, RoutedEventArgs e)
{

    /** Clear the results field */
    txtResult.Text = "";

    /** Disable the button and show waiting status */
    btnAction.IsEnabled = false;
    lblStatus.Text = "Wait...";

    /** Get input from the query field */
    string input = query.Text;

    /** Run a new task */
    Task.Run(() => {

        // calling a method that takes a long time (>3s) to finish and return
        var attempt = someLibrary.doSomethingWith(input);

        // return the result to the GUI thred
        this.Dispatcher.Invoke(() =>
        {

            if (attempt.ContainsKey("success"))
            {
                if (attempt["success"] == true)
                {
                    txtResult.Text = "Success! The door is: " + (attempt["is_open"] ? "open" : "closed");
                    lblStatus.Text = "";
                }
                else
                {
                    lblStatus.Text = "Error! The service says: " + attempt["errorMessage"];
                }
            }

            else
            {
                MessageBox.Show("There was a problem getting results from web service.");
                lblStatus.Text = "";
            }

            /** Re-enable the button */
            btnAction.IsEnabled = true;

        });
    });

}

现在,我想要:
  • 使用回调来编写相同的代码,而不是使用 Dispatcher.Invoke()
  • 能够取消调用 doSomething()的正在运行的任务
  • 能够链接多个调用,即等待 doSomething()完成后,使用前一个调用的结果来执行 doAnotherThing()
因此,我想使用异步模型来编写这个代码。
我该怎么做?

1
定义“能够链接多个调用” - TheGeneral
2
@MichaelRandall:这是使用Task.Run发生的情况,但问题是如何使事件处理程序异步并且使用Task.Run。进行更改的动机可能是因为必须使用Dispatcher.Invoke返回到UI线程。 - madreflection
1
Dispatcher.Invoke() 完全不需要使用。如果你使用 var attempt = await Task.Run(() => someLibrary.doSomethingWith(input));,你可以使用结果来设置UI。这不会花费时间。就是在重复我之前评论中写的内容。 - Jimi
1
@madreflection 这里没有什么需要异步处理的好处。嗯,显然是有的。能够链接多个调用 部分需要最终进行讨论。如果您正在更新相同的控件,那么您要链接什么?或者,还有其他用途吗?也许这个过程必须重复无数次?然后,弹出消息框...? - Jimi
1
我想说的是,除了OP想要删除的Task.Run之外,那段代码中没有任何可等待的内容。 - madreflection
显示剩余5条评论
2个回答

5
你需要将你的方法标记为async,然后awaitTask.Run,这样continuations可以在UI上运行,并且只有long running(看起来是CPU绑定的)工作留在其中。
private async void btnAction_Click(object sender, RoutedEventArgs e)
{
   btnAction.IsEnabled = false;
   txtResult.Text = "";       
   lblStatus.Text = "Wait...";

   string input = query.Text;

   // calling a method that takes a long time (>3s) to finish and return
   var attempt =  await Task.Run(() => someLibrary.doSomethingWith(input));

   if (attempt.ContainsKey("success"))
   {
      if (attempt["success"] == true)
      {
         txtResult.Text = "Success! The door is: " + (attempt["is_open"] ? "open" : "closed");
         lblStatus.Text = "";
      }
      else
      {
         lblStatus.Text = "Error! The service says: " + attempt["errorMessage"];
      }
   }  
   else
   {
      MessageBox.Show("There was a problem getting results from web service.");
      lblStatus.Text = "";
   }

   btnAction.IsEnabled = true;

}

更新

要取消任务,您需要使用来自CancellationTokenSource实例的CancellationToken并将其传递到Task.Run以及您的长时间运行的方法中,以检查IsCancellationRequested(如果可能)。您可以通过调用CancellationTokenSource.Cancel来进行取消。

请注意,您可能希望将此包装在 try catch finally 中,并在OperationCanceledException上进行捕获,并将您的按钮启用代码放置在finally中。


1
完美。我想要添加两件事:1. 使用 Task.Factory.StartNew 而不是 Task.Run (await Task.Factory.StartNew(...); UpdateUi(...); await Task.Factory.StartNew(...); UpdateUi(...); ...) 2. 禁用按钮是第一步!总是这样! 相信我,如果不这样做,我可以双击。 ;) - Alb
这是完美的,因为它消除了对Dispatcher.Invoke()的不必要调用!它还解决了链式调用的问题,因为现在我可以使用await来等待Task.Run()。为了完整起见,有没有一种方法可以取消正在运行的任务(可能会抛出异常)? - ConfusedGuy
1
@ConfusedGuy 如果你想将长时间运行的任务从线程池中移除,可以使用StartNew方法。这是一种较旧的方法,具有更多细粒度的提示来控制其行为。但我个人建议现在只使用更新的Task.Run,除非你遇到了线程池资源问题。要取消任务,你需要将令牌传递给Task.Run,但一定要捕获异常。 - TheGeneral
1
@ConfusedGuy 是的,应该声明一个 CancellationTokenSource,然后获取它的 .Token。我不会让任务处理它,而是直接作为第二个参数传递给 doSomethingWith()。在内部,我会编写 token.ThrowIfCancellationRequested();(特别是在循环中需要进行检查)。既然我们在这里:你应该知道所有的 await 都应该用 try-finally 包装(或者 try-catch),因为它们不会冒泡到最顶层的调用者!例如:try { await Task.Run(async () => await Task.Run(() => throw new Exception())); } catch {} 在发布模式下没有被捕获并且会导致应用程序崩溃。 - Alb
@ConfusedGuy 你可以创建第二个(虚假的)任务(只在取消时返回),作为主任务的竞争对手,并使用 Task.WhenAny()。但这只能让你在主线程上执行更多操作,而不能停止其他任务。除非你正在关闭应用程序(线程无论如何都会关闭)或者你可能忘记了网络调用,否则这是唯一可接受的。因为:1. 你可能允许用户重新启动仍在后台运行的某些内容。2. 如果缺少try { await ...; } catch {},你会失去鲁棒性。 - Alb
显示剩余9条评论

2
“async”修饰符要求函数返回“Task”(或“void”,在这种情况下,任何等待语句都将被忽略)。这意味着使用“async”和使用“Task.Run()”是一样的,你提出的问题前提没有意义。
然而,我认为你想做的只是使用“async await”语法来避免显式调用“Task.Run()”。
回调
只需创建一个返回“Task”的函数即可。
async Task<T> Foo()

然后赋值一个变量 var bar=await Foo();

取消正在运行的任务

只需使用CancellationToken

CancellationTokenSource tokenSource = new CancellationTokenSource();

如果你使用两个参数构建一个任务,第二个参数是取消令牌:

var bar= new Task(action, tokenSource.Token)

这让您可以使用tokenSource.Cancel();。
相关链接:https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken?view=netframework-4.8 链接调用
如果您不需要执行的定义顺序,可以使用Task.WhenAll(),否则可以在前一个任务中或在等待代码中执行下一个任务。
Task.WhenAll(): https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.whenall?view=netframework-4.8#System_Threading_Tasks_Task_WhenAll_System_Collections_Generic_IEnumerable_System_Threading_Tasks_Task__

谢谢你指引我正确的方向。那么,在这种情况下,调用 tokenSource.Cancel() 会取消 bar 吗?那么会发生什么,会抛出异常吗?此外,在这种情况下,我确实需要链接调用,因为第一次调用的结果是第二次调用的参数。所以我不能在这里同时异步调用两个方法。 - ConfusedGuy

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