【问题标题】:Using async instead of Task.Run()使用异步而不是 Task.Run()
【发布时间】:2020-02-19 01:09:45
【问题描述】:

我有以下代码:

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() 与上一次调用的结果

因此我想使用异步模型来编写这个。

我该怎么办?

【问题讨论】:

  • define "能够链接多个调用"
  • @MichaelRandall:Task.Run 会发生这种情况,但问题在于使事件处理程序异步并且使用Task.Run。这种变化的推动力可能是因为必须使用Dispatcher.Invoke 来返回 UI 线程。
  • @madreflection Dispatcher.Invoke() 根本不需要使用。如果您var attempt = await TaskRun(() => someLibrary.doSomethingWith(input));,您可以使用结果来设置 UI。这不需要时间。好吧,只是重复我在之前的评论中写的内容。
  • @madreflection 这里没有什么可以从异步中受益。嗯,显然有。 能够链接多个调用部分是最终需要讨论的部分。如果您要更新相同的控件,您会链接什么?或者,它还有其他用途吗?也许这个程序必须随意重复?那么,MessageBox...?
  • 我的意思是,除了 OP 希望删除的 Task.Run 之外,该代码中没有任何内容是可等待的。

标签: c# async-await


【解决方案1】:

您可以将您的方法标记为asyncawait Task.Run,因此 continuationsUI 上运行,也只留下 long在其中运行(似乎受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。使用Task.Factory.StartNew 而不是Task.Run (await Task.Factory.StartNew(...); UpdateUi(...); await Task.Factory.StartNew(...); UpdateUi(...); ...) 2.禁用按钮是第一行!总是!相信我,如果没有,我可以双击。 ;)
  • @Alb 为什么我们使用StartNew
  • 这是完美的,因为它消除了对Dispatcher.Invoke() 的不必要调用!它还解决了链接调用的问题,因为现在我可以await Task.Run()。为了完整起见,有没有办法取消正在运行的任务(可能会抛出异常)?
  • @ConfusedGuy 你会使用 StartNew,如果你想从线程池中获取长时间运行的任务,它是一种较旧的方法,有更多细粒度的提示来控制其行为,我会除非您遇到线程池资源问题,否则个人暂时只使用较新的Task.Run。要取消任务,请将令牌传递给Task.Run,但请确保捕获异常
  • @ConfusedGuy 是的,应该声明一个CancellationTokenSource,然后取它的.Token。我不会让任务处理它,而是直接传递给doSomethingWith() 作为第二个参数。在里面,我会写token.ThrowIfCancellationRequested();(特别是在循环中需要检查)。既然我们在那里:你应该知道所有等待都应该用try-finally(或try-catch)包装,因为它们不会冒泡到最顶层的调用者!示例:try { await Task.Run(async () => await Task.Run(() => throw new Exception())); } catch {} 未进入发布模式并导致您的应用崩溃
【解决方案2】:

async 修饰符要求函数返回Task<T>(或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://docs.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken?view=netframework-4.8

链接调用

如果您不需要定义的执行顺序,您可以使用Task.WhenAll(),否则您可以在前一个任务中或在等待代码中执行下一个任务。

Task.WhenAll():https://docs.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?那么会发生什么,会抛出异常吗?此外,在这种情况下,我确实需要链接调用,因为第一次调用的结果是第二次调用的参数。所以这里不能异步调用这两种方法
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-01
  • 2019-02-26
  • 1970-01-01
  • 2015-12-19
相关资源
最近更新 更多