如何从WPF gui运行异步任务并与之交互 [英] How to run and interact with an async Task from a WPF gui
问题描述
我有一个WPF GUI,在这里我想按一个按钮来启动一个长任务,而又不冻结任务期间的窗口.在任务运行时,我想获取进度报告,并且希望合并另一个按钮,该按钮将在我选择的任何时间停止任务.
我不知道使用异步/等待/任务的正确方法.我无法提供我尝试过的所有内容,但这就是我目前所拥有的.
WPF窗口类:
public partial class MainWindow : Window
{
readonly otherClass _burnBabyBurn = new OtherClass();
internal bool StopWorking = false;
//A button method to start the long running method
private async void Button_Click_3(object sender, RoutedEventArgs e)
{
Task slowBurn = _burnBabyBurn.ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);
await slowBurn;
}
//A button Method to interrupt and stop the long running method
private void StopButton_Click(object sender, RoutedEventArgs e)
{
StopWorking = true;
}
//A method to allow the worker method to call back and update the gui
internal void UpdateWindow(string message)
{
TextBox1.Text = message;
}
}
还有一个用于worker方法的类:
class OtherClass
{
internal Task ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
{
var tcs = new TaskCompletionSource<int>();
//Start doing work
gui.UpdateWindow("Work Started");
While(stillWorking)
{
//Mid procedure progress report
gui.UpdateWindow("Bath water n% thrown out");
if (gui.StopTraining) return tcs.Task;
}
//Exit message
gui.UpdateWindow("Done and Done");
return tcs.Task;
}
}
这会运行,但是一旦worker方法启动,WPF功能窗口仍会被阻止.
我需要知道如何安排异步/等待/任务声明以允许
A)worker方法不阻止gui窗口
B)让worker方法更新gui窗口
C)允许gui窗口停止中断并停止worker方法
非常感谢任何帮助或指针.
长话短说:
private async void ButtonClick(object sender, RoutedEventArgs e)
{
// modify UI object in UI thread
txt.Text = "started";
// run a method in another thread
await HeavyMethod(txt);
// <<method execution is finished here>>
// modify UI object in UI thread
txt.Text = "done";
}
// This is a thread-safe method. You can run it in any thread
internal async Task HeavyMethod(TextBox textBox)
{
while (stillWorking)
{
textBox.Dispatcher.Invoke(() =>
{
// UI operation goes inside of Invoke
textBox.Text += ".";
// Note that:
// Dispatcher.Invoke() blocks the UI thread anyway
// but without it you can't modify UI objects from another thread
});
// CPU-bound or I/O-bound operation goes outside of Invoke
// await won't block UI thread, unless it's run in a synchronous context
await Task.Delay(51);
}
}
Result:
started....................done
您需要了解(1)如何编写async
代码和(2)如何在另一个线程中运行UI操作.
async
和await
的魔力:
-
您只能在
async
方法中使用await
. -
您只能
await
一个awaitable
对象(即Task
,Task<T>
或ValueTask<T>
等) -
async
方法的返回类型包装在Task
中,而await
对其进行解包. (请参阅包装和展开部分) -
Task.Run
通常通常在线程池Task
排队
异步方法的返回类型必须为
void
,Task
,Task<T>
,类似任务的类型,IAsyncEnumerable<T>
或IAsyncEnumerator<T>
如果您不想了解更多信息,则可能需要向下滚动至 WPF GUI 部分!
包装和展开:
当您从async
方法中return
值时,它将被包装在Task<T>
中.例如,如果您期望async
方法中的int
,那么它应该返回Task<int>
:
private async Task<int> GetOneAsync()
{
return 1; // return type is a simple int
// while the method signature indicates a Task<int>
}
要检索或解包在Task<>
内包装的值:
- 异步选项:
await
- 同步选项:
task.Result
或task.GetAwaiter().GetResult()
或task.WaitAndUnwrapException()
或阅读 MSDN .要展开任务结果,请始终尝试使用
await
而不是.Result
,否则将没有异步的好处,而只有异步的缺点.注意:
await
是异步的,与同步的task.Wait()
不同.但是他们俩都在做同样的事情,等待任务完成.await
是异步的,与同步的task.Result
不同.但是他们俩都在做同样的事情,就是等待任务完成,展开并返回结果.要具有包装值,可以始终使用
Task.FromResult(1)
而不是通过使用Task.Run(() => 1)
创建新线程.Task.Run
是更新的(.NetFX4.5)和Task.Factory.StartNew
的简单版本命名约定
使用
Async
关键字用async
关键字简单地后缀方法名称.因为避免使用
async void
方法是一种好习惯(请参见下面的模式),所以可以说仅Task
返回的方法应后缀Async
.此约定的目的是确保遵守异步病毒.
WPF GUI:
这是我解释如何在另一个线程中运行UI操作的地方.
阻止:
关于 WPF异步事件处理程序,您需要了解的第一件事是
Dispatcher
将提供在此处解释与CPU绑定或与IO绑定的操作(如
Sleep
和task.Wait()
)将阻塞并消耗线程,即使在使用async
关键字的方法中调用了它们也是如此.但是await Task.Delay()
告诉状态机停止在线程上执行流,以便它不消耗线程;意味着线程资源可以在其他地方使用:private async void Button_Click(object sender, RoutedEventArgs e) { Thread.Sleep(1000);//stops, blocks and consumes threadpool resources await Task.Delay(1000);//stops without consuming threadpool resources Task.Run(() => Thread.Sleep(1000));//does not stop but consumes threadpool resources await Task.Run(() => Thread.Sleep(1000));//literally the WORST thing to do }
线程安全:
如果必须异步访问GUI(在
ExecuteLongProcedure
方法内部),请调用任何涉及修改任何非线程安全对象的操作.例如,必须使用与GUI线程关联的Dispatcher
对象来调用任何WPF GUI对象:void UpdateWindow(string text) { //safe call Dispatcher.Invoke(() => { txt.Text += text; }); }
但是,如果由于ViewModel中的属性更改回调而启动了任务,则无需使用
Dispatcher.Invoke
,因为该回调实际上是从UI线程执行的./p>访问非UI线程上的集合
使用WPF,您可以在创建集合的线程之外的线程上访问和修改数据集合.这使您可以使用后台线程从外部源(例如数据库)接收数据,并在UI线程上显示数据.通过使用另一个线程修改集合,您的用户界面将保持对用户交互的响应.
由INotifyPropertyChanged触发的值更改将自动编组回调度程序.
TaskScheduler.UnobservedTaskException
.void Do() { // CPU-Bound or IO-Bound operations } async Task DoAsync() // returns Task { await Task.Run(Do); } void FireAndWait() // not blocks, not waits { Task.Run(DoAsync); }
在浪费线程资源的同时同步触发并等待:
这称为通过异步同步,它是一种同步操作,但它使用多个线程可能会导致饥饿.当您调用
Wait()
或尝试在任务完成之前直接从task.Result
读取结果时,就会发生这种情况.(避免使用此模式)
void Do() { // CPU-Bound or IO-Bound operations } async Task DoAsync() // returns Task { await Task.Run(Do); } void FireAndWait() // blocks, waits and uses 2 more threads. Yikes! { var task = Task.Run(DoAsync); task.Wait(); }
这就是全部吗?
不.关于
async
,其上下文和其延续,还有很多要学习的内容.特别推荐此博客.任务使用线程?确定吗?
不一定.阅读此答案,以了解有关
async
真实面孔的更多信息.Stephen Cleary 完美地解释了
async-await
.他还在其他博客帖子中进行了解释,不涉及线程.了解更多
how-to-call-asynchronous-method -from-synchronous-method
确保您知道异步,并行和并发之间的区别.
您还可以阅读一个简单的异步文件编写器,以了解应该在何处并发.
调查并发名称空间
最后,请阅读此电子书: Patterns_of_Parallel_Programming_CSharp
I have a WPF GUI, where I want to press a button to start a long task without freezing the window for the duration of the task. While the task is running I would like to get reports on progress, and I would like to incorporate another button that will stop the task at any time I choose.
I cannot figure the correct way to use async/await/task. I can't include everything I've tried, but this is what I have at the moment.
A WPF window class :
public partial class MainWindow : Window { readonly otherClass _burnBabyBurn = new OtherClass(); internal bool StopWorking = false; //A button method to start the long running method private async void Button_Click_3(object sender, RoutedEventArgs e) { Task slowBurn = _burnBabyBurn.ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3); await slowBurn; } //A button Method to interrupt and stop the long running method private void StopButton_Click(object sender, RoutedEventArgs e) { StopWorking = true; } //A method to allow the worker method to call back and update the gui internal void UpdateWindow(string message) { TextBox1.Text = message; } }
And a class for the worker method:
class OtherClass { internal Task ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3) { var tcs = new TaskCompletionSource<int>(); //Start doing work gui.UpdateWindow("Work Started"); While(stillWorking) { //Mid procedure progress report gui.UpdateWindow("Bath water n% thrown out"); if (gui.StopTraining) return tcs.Task; } //Exit message gui.UpdateWindow("Done and Done"); return tcs.Task; } }
This runs, but the WPF function window is still blocked once the worker method starts.
I need to know how to arrange the async/await/task declarations to allow
A) the worker method to not block the gui window
B) let the worker method update the gui window
C) allow the gui window to stop interrupt and stop the worker methodAny help or pointers are much appreciated.
解决方案Long story short:
private async void ButtonClick(object sender, RoutedEventArgs e) { // modify UI object in UI thread txt.Text = "started"; // run a method in another thread await HeavyMethod(txt); // <<method execution is finished here>> // modify UI object in UI thread txt.Text = "done"; } // This is a thread-safe method. You can run it in any thread internal async Task HeavyMethod(TextBox textBox) { while (stillWorking) { textBox.Dispatcher.Invoke(() => { // UI operation goes inside of Invoke textBox.Text += "."; // Note that: // Dispatcher.Invoke() blocks the UI thread anyway // but without it you can't modify UI objects from another thread }); // CPU-bound or I/O-bound operation goes outside of Invoke // await won't block UI thread, unless it's run in a synchronous context await Task.Delay(51); } }
Result: started....................done
You need to know about (1) how to write
async
code and (2) how to run UI operations in another thread.The magic of
async
andawait
:You can only
await
in anasync
method.You can only
await
anawaitable
object (i.e.Task
,Task<T>
orValueTask<T>
etc)The return type of an
async
method is wrapped in aTask
andawait
unwraps it. (see Wrapping and Unwrapping section)Task.Run
usually queues aTask
in the thread pool
(i.e. it uses an existing thread or creates a new thread in the thread pool to run the task. Unless it's a pure operation)
The execution waits at
await
for the task to finish and returns back its results, without blocking the main thread because of the magic:The magic of
async-await
is that it uses a state-machine to let the compiler give up and take back the control over theawaited Task
in anasync
method.(i.e.
async
method does not run in another thread.async
andawait
by themselves don't have anything to do with thread creation.)
Don't confuse the method with
async
keyword with the method wrapped within aTask
; TheTask
is responsible for threading, theasync
is responsible for the magicSo
By putting
async
in the method signature you tell the compiler to use state machine to call this method (no threading so far). Then by running aTask
you use a thread to call the method inside the task. And byawait
ing the task you prevent the execution flow to move past theawait
line without blocking UI thread.The event handler looks like the code below.
Two possible cases for presense of async in the signature of
ExecuteLongProcedure
(case 1 and 2) andMyButton_Click
(case A and B) are explained:private async void MyButton_Click(object sender, RoutedEventArgs e) { //queue a task to run on threadpool // 1. if ExecuteLongProcedure is a normal method and returns void Task task = Task.Run(()=> ExecuteLongProcedure(this, intParam1, intParam2, intParam3) ); // or // 2. if ExecuteLongProcedure is an async method and returns Task Task task = ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3); // either way ExecuteLongProcedure is running asynchronously here // the method will exit if you don't wait for the Task to finish // A. wait without blocking the main thread // -> requires MyButton_Click to be async await task; // or // B. wait and block the thread (NOT RECOMMENDED AT ALL) // -> does not require MyButton_Click to be async task.Wait(); }
Return Types:
This is very important to know. Suppose you have the following declaration:
private async ReturnType Method() { ... }
If
ReturnType
isvoid
you can'tawait
it- If you try writing
await Method();
, you will get a compile error saying cannot await void. - You can only fire and forget i.e. just call the method normally:
Method();
and then go on with your life. - The
Method
execution will be synchronous, however since it hasasync
it will allow you to take advantage of the magic, i.e. you can writeawait task
within the method to control the flow of execution. - This is how WPF handles your button click event handler, obviously because your event handler returns
void
.
- If you try writing
If
ReturnType
isTask
thenawait Method();
returnsvoid
If
ReturnType
isTask<T>
thenawait Method();
returns a value of typeT
The return type of an async method must be
void
,Task
,Task<T>
, a task-like type,IAsyncEnumerable<T>
, orIAsyncEnumerator<T>
You might want to scroll down to WPF GUI section if you don't want to learn more!
Wrapping and Unrwapping:
When you
return
a value from anasync
method it will be wrapped in aTask<T>
. E.g., if you expect anint
from yourasync
method then it should return aTask<int>
:private async Task<int> GetOneAsync() { return 1; // return type is a simple int // while the method signature indicates a Task<int> }
To retrieve or unwrap the value which is wrapped inside a
Task<>
:- asynchronous option:
await
- synchronous option:
task.Result
ortask.GetAwaiter().GetResult()
ortask.WaitAndUnwrapException()
or read How to call asynchronous method from synchronous method in C#?
e.g.
int number = await Task.Run(() => 1); // right hand of await is a Task<int> // while the left hand is an int
expand it like this:
Func<int> GetOneFunc = () => 1; // synchronous function returning a number Task<int> GetOneTask = Task.Run(GetOneFunc); // a Task<int> is started int number = await GetOneTask; // waiting AND unwrapping Task<int> into int
The whole code:
private async Task<int> GetNumberAsync() { int number = await Task.Run(GetNumber); // unwrap int from Task<int> // int number = Task.FromResult(1); // the correct way for short operations // bad practices: // int number = Task.Run(GetNumber).GetAwaiter().GetResult(); // sync over async // int number = Task.Run(GetNumber).Result; // sync over async // int number = Task.Run(GetNumber).Wait(); // sync over async return number; // wrap int in Task<int> }
Still confused? Read async return types on MSDN.
To unwrap a task result, Always try to use
await
instead of.Result
otherwise there will be no asynchronous benefit but only asynchronous disadvantages.Note:
await
is a asynchronous and is different fromtask.Wait()
which is synchronous. But they both do the same thing which is waiting for the task to finish.await
is a asynchronous and is different fromtask.Result
which is synchronous. But they both do the same thing which is waiting for the task to finish and unwrapping and returning back the results.To have a wrapped value, you can always use
Task.FromResult(1)
instead of creating a new thread by usingTask.Run(() => 1)
.Task.Run
is newer (.NetFX4.5) and simpler version ofTask.Factory.StartNew
Naming Convention
Simply postfix the name of the method with the
async
keyword withAsync
.Since avoiding
async void
methods is a good practice (see patterns below), you can say onlyTask
returning methods should be postfixed withAsync
.The purpose of this convention is to make sure the Asynchronous Virality is respected.
WPF GUI:
This is where I explain how to run UI operations in another thread.
Blocking:
First thing you need to know about WPF async event handlers is that the
Dispatcher
will provide a synchronization context. Explained hereCPU-bound or IO-bound operations such as
Sleep
andtask.Wait()
will block and consume the thread even if they are called in a method withasync
keyword. butawait Task.Delay()
tells the state-machine to stop the flow of execution on the thread so it does not consume it; meaning that the thread resources can be used elsewhere:private async void Button_Click(object sender, RoutedEventArgs e) { Thread.Sleep(1000);//stops, blocks and consumes threadpool resources await Task.Delay(1000);//stops without consuming threadpool resources Task.Run(() => Thread.Sleep(1000));//does not stop but consumes threadpool resources await Task.Run(() => Thread.Sleep(1000));//literally the WORST thing to do }
Thread Safety:
If you have to access GUI asynchronously (inside
ExecuteLongProcedure
method), invoke any operation which involves modification to any non-thread-safe object. For instance, any WPF GUI object must be invoked using aDispatcher
object which is associated with the GUI thread:void UpdateWindow(string text) { //safe call Dispatcher.Invoke(() => { txt.Text += text; }); }
However, If a task is started as a result of a property changed callback from the ViewModel, there is no need to use
Dispatcher.Invoke
because the callback is actually executed from the UI thread.Accessing collections on non-UI Threads
WPF enables you to access and modify data collections on threads other than the one that created the collection. This enables you to use a background thread to receive data from an external source, such as a database, and display the data on the UI thread. By using another thread to modify the collection, your user interface remains responsive to user interaction.
Value changes fired by INotifyPropertyChanged are automatically marshalled back onto the dispatcher.
How to enable cross-thread access
Remember,
async
method itself runs on the main thread. So this is valid:private async void MyButton_Click(object sender, RoutedEventArgs e) { txt.Text = "starting"; // UI Thread await Task.Run(()=> ExecuteLongProcedure1()); txt.Text = "waiting"; // UI Thread await Task.Run(()=> ExecuteLongProcedure2()); txt.Text = "finished"; // UI Thread }
Patterns:
Fire and forget pattern:
For obvious reasons this is how your WPF GUI event handlers such as
Button_Click
are called.void Do() { // CPU-Bound or IO-Bound operations } async void DoAsync() // returns void { await Task.Run(Do); } void FireAndForget() // not blocks, not waits { DoAsync(); }
Fire and observe:
Task-returning methods are better since unhandled exceptions trigger the
TaskScheduler.UnobservedTaskException
.void Do() { // CPU-Bound or IO-Bound operations } async Task DoAsync() // returns Task { await Task.Run(Do); } void FireAndWait() // not blocks, not waits { Task.Run(DoAsync); }
Fire and wait synchronously while wasting thread resources:
This is known as Sync over async, it is a synchronous operation but it uses more than one thread which may cause starvation. This happens when you call
Wait()
or try to read results directly fromtask.Result
before the task is finished.(AVOID THIS PATTERN)
void Do() { // CPU-Bound or IO-Bound operations } async Task DoAsync() // returns Task { await Task.Run(Do); } void FireAndWait() // blocks, waits and uses 2 more threads. Yikes! { var task = Task.Run(DoAsync); task.Wait(); }
Is this all to it?
No. There is a lot more to learn about
async
, its context and its continuation. This blogpost is especially recommended.Task uses Thread? Are you sure?
Not necessarily. Read this answer to know more about the true face of
async
.Stephen Cleary has explained
async-await
perfectly. He also explains in his other blog post when there is no thread involved.Read more
how-to-call-asynchronous-method-from-synchronous-method
async await
- Behind the scenesMake sure you know the difference between Asynchronous, Parallel and Concurrent.
You may also read a simple asynchronous file writer to know where you should concurrent.
Investigate concurrent namespace
Ultimately, read this e-book: Patterns_of_Parallel_Programming_CSharp
这篇关于如何从WPF gui运行异步任务并与之交互的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!