使用async而不是Task.Run()
使用async而不是Task.Run()
我有以下的代码段:\n
private void btnAction_Click(object sender, RoutedEventArgs e) { /** 清空结果字段 */ txtResult.Text = ""; /** 禁用按钮并显示等待状态 */ btnAction.IsEnabled = false; lblStatus.Text = "等待..."; /** 从查询字段获取输入 */ string input = query.Text; /** 运行一个新的任务 */ Task.Run(() => { // 调用一个需要较长时间(>3秒)才能完成并返回的方法 var attempt = someLibrary.doSomethingWith(input); // 将结果返回给GUI线程 this.Dispatcher.Invoke(() => { if (attempt.ContainsKey("success")) { if (attempt["success"] == true) { txtResult.Text = "成功!门是:" + (attempt["is_open"] ? "打开的" : "关闭的"); lblStatus.Text = ""; } else { lblStatus.Text = "错误!服务返回:" + attempt["errorMessage"]; } } else { MessageBox.Show("从网络服务获取结果时出现问题。"); lblStatus.Text = ""; } /** 启用按钮 */ btnAction.IsEnabled = true; }); }); }
\n现在,我想要:\n
- \n
- 使用回调函数而不是使用
Dispatcher.Invoke()
来编写相同的代码。 - 能够取消调用
doSomething()
的运行中任务。 - 能够链接多个调用,即等待
doSomething()
完成后,使用之前调用的结果调用doAnotherThing()
。
\n
\n
\n
\n因此,我想要使用异步模型来编写代码。\n我该怎么做?
问题的原因是使用async
修饰符要求函数返回Task<T>
(或void
,在这种情况下,任何await
语句将被忽略)。这意味着使用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();
链接调用
如果你不需要定义执行顺序,可以使用Task.WhenAll()
,否则可以在前一个任务中执行下一个任务或在等待的代码中执行。
感谢你指引我正确的方向。那么,在这种情况下,调用tokenSource.Cancel()
会取消bar
吗?然后会发生什么,会抛出异常吗?还有,在这个上下文中,我确实需要链接调用,因为第一个调用的结果是第二个调用的参数。所以我不能同时异步调用这两个方法。
使用async
而不是Task.Run()
的原因是为了在UI线程上运行continuations,同时只将长时间运行(看起来是CPU密集型)的任务留在其中。
问题的解决方法是将方法标记为async
,使用await
等待Task.Run
,以便continuations在UI上运行,同时在其中只留下长时间运行(看起来是CPU密集型)的任务。
要取消任务,可以使用来自CancellationTokenSource
实例的CancellationToken
,并将其传递给Task.Run
和长时间运行的方法,以检查IsCancellationRequested
(如果可以的话)。可以通过调用CancellationTokenSource.Cancel
来取消。
完美。有两件事我想要补充:1. 使用Task.Factory.StartNew
而不是Task.Run
。(await Task.Factory.StartNew(...); UpdateUi(...); await Task.Factory.StartNew(...); UpdateUi(...); ...
)2. 禁用按钮是第一行!一定要!相信我,如果不这样做,我可以双击。
我们为什么要使用StartNew
?
这是完美的,因为它消除了不必要的对Dispatcher.Invoke()
的调用!它还解决了链接调用的问题,因为现在我可以await
Task.Run()
。为了完整起见,有没有办法取消正在运行的任务(可能会抛出异常)?
好的,将它从线程池中移除,我个人认为除非需要,否则不必担心,不过还是感谢这个提醒。
1. 研究一下Task.Factory.StartNew()
,不确定有什么区别,另外2. 哇,这很有意思!我猜这是有道理的。从现在开始,我会确保始终在第一行将按钮禁用
这很棒-不过由于我无法在doSomethingWith
中检查IsCancellationRequested
,因为它由someLibrary
处理。有没有办法强制停止任务并引发异常?
你可以使用StartNew
,如果你想要将长时间运行的任务从线程池中移除,它是一种更旧的方法,具有更多细粒度的提示来控制其行为,我个人现在只会使用更新的Task.Run
,除非你在使用线程池资源时遇到问题。要取消任务,你将令牌传递给Task.Run
,但一定要捕获异常。
是的,应该声明一个CancellationTokenSource
,然后使用它的.Token
。我不会让任务处理它,而是直接传递给doSomethingWith()
作为第二个参数。在内部,我会写token.ThrowIfCancellationRequested();
(在循环中特别需要进行检查)。还有,既然我们已经提到了:你应该知道所有的await都应该用try-finally(或try-catch)包裹,因为它们不会上升到最顶层的调用者!示例:try { await Task.Run(async () => await Task.Run(() => throw new Exception())); } catch {}
在发布模式下不会被捕获并会导致应用程序崩溃。
如果你可以访问SomeLibrary,你应该重构代码,添加第二个CancellationToken
参数,因为只要不需要它,你只需传入CancellationToken.None
。
我知道有一点,就是没有为任务实现Task.Abort
,可能是因为不可靠的Thread.Abort
无法依赖(不是Abort本身,而是你在该线程上执行的内容)。
你可以创建第二个(虚假的)任务(只在取消时返回),作为主任务的对手,并使用Task.WhenAny()
。但这只能让你在主线程上继续执行,而不能停止另一个任务。只有在关闭应用程序(线程已经关闭)或者你可以忘记一个网络调用的情况下才能接受。因为:1.:你可能允许用户重新启动仍在后台运行的任务。2.:通过丢失try { await ...; } catch {}
来失去鲁棒性。
Task.Run
对于这种情况优于StartNew
。