我正在尝试构建一个应用来评估公司产品。为安全起见,下面的描述在某种程度上是抽象的。
此应用程序的作用是更改产品中的某个参数,并查看产品的某个值如何变化。所以我需要做两件事。
图表是这样的。
这些任务应该重复,直到按下取消按钮。
UI具有以下控件:
所以这是我写的代码。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
CancellationTokenSource cts = new CancellationTokenSource();
private async void button1_Click(object sender, EventArgs e)
{
await Task1();
await Task2();
}
private async Task Task1()
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(500);
ChangeParameter(0);
Thread.Sleep(1000);
ChangeParameter(10);
Thread.Sleep(500);
ChangeParameter(0);
}
}
private void ChangeParameter(double param)
{
// change device paremeter
Console.WriteLine("devicep parameter changed : " + param);
}
private async Task Task2()
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(100);
int data = GetDataFromDevice();
UpdateTextBoxWithData(data);
}
cts.Token.ThrowIfCancellationRequested();
}
private int GetDataFromDevice()
{
//pseudo code
var rnd = new Random();
return rnd.Next(100);
}
private void UpdateTextBoxWithData(int data)
{
textBox1.AppendText(data.ToString() + "\n");
// debug
Console.WriteLine("data : " + data);
}
private void button2_Click(object sender, EventArgs e)
{
cts.Cancel();
}
}
但是,此代码中存在两个问题。
第二个问题源自await
,因为它逐个执行任务。我本可以使用Task.Run()
但这不允许向textBox添加值,因为它与UI线程不同。
我该如何解决这些问题?任何帮助将不胜感激。
答案 0 :(得分:2)
问题是您没有在任务中使用await
,因此他们同步执行 。
你应该使用这样的东西来维持你的UI响应(注意这不是生产代码,我只是在表达一个想法):
private void button1_Click(object sender, EventArgs e)
{
try
{
await Task.WhenAll(Task1(cts.Token), Task2(cts.Token));
}
catch (TaskCancelledException ex)
{
}
}
private async Task Task1(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested();
await Task.Delay(500, token); // pass token to ensure delay canceled exactly when cancel is pressed
ChangeParameter(0);
await Task.Delay(1000, token);
ChangeParameter(10);
await Task.Delay(500, token);
ChangeParameter(0);
}
}
private async Task Task2(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested();
await Task.Delay(100, token);
int data = await Task.Run(() => GetDataFromDevice()); //assuming this could be long running operation it shouldn't be on ui thread
UpdateTextBoxWithData(data);
}
}
基本上,当您需要在背景上运行某些内容时,您应该将其包含在Task.Run()
中,然后await
中以获得结果。只需将async
添加到您的方法中,就不会使此方法异步。
为了让您的代码更加清晰,我建议您将GetDataFromDevice
或ChangeParameter
等方法移至服务层。另外,请查看IProgress
,因为评论建议根据某个流程的进度更新您的用户界面。
答案 1 :(得分:2)
此代码存在许多问题:
async/await
不会使代码自动异步。它允许您等待已经异步操作的结果。如果你想在后台运行一些不是异步的东西,你需要使用Task.Run或类似的方法来启动任务。await
将执行返回到原始同步上下文。在这种情况下,UI线程。通过使用Thread.Sleep,您将冻结UI线程 Maxim Kosov已经清理了代码,并展示了如何正确使用async/await
和Task.Run,因此我只会发布如何使用IProgress< T>及其实施,{{3} }
IProgress用于使用Progress< T> 方法公布进度更新。它的默认实现Progress在UI线程上引发IProgress< T>.Report事件和/或调用传递给其构造函数的Action<T>
。具体来说,在创建类时捕获的同步上下文。
您可以在构造函数或按钮单击事件中创建进度对象,例如
private async void button1_Click(object sender, EventArgs e)
{
var progress=new Progress<int>(data=>UpdateTextBoxWithData(data));
//...
//Allow for cancellation of the task itself
var token=cts.Token;
await Task.Run(()=>MeasureInBackground(token,progress),token);
}
private async Task MeasureInBackground(CancellationToken token,IProgress<int> progress)
{
while (!token.IsCancellationRequested)
{
await Task.Delay(100,token);
int data = GetDataFromDevice();
progress.Report(data);
}
}
请注意,在任务中使用Thread.Sleep
并不是一个好主意,因为它会浪费线程池线程不执行任何操作。最好使用await Task.Delay()
,这要求方法的签名更改为async Task
。为此目的,有一个ProgressChanged重载。
该方法与Maxim Kosov的代码略有不同,表明IProgress确实跨线程进行通信。 IProgress可以处理复杂的类,因此您可以返回进度百分比和消息,例如:
private async Task MeasureInBackground(CancellationToken token,IProgress<Tuple<int,string>> progress)
{
while(!token.IsCancellationRequested)
{
await Task.Delay(100,token);
int data = GetDataFromDevice();
progress.Report(Tuple.Create(data,"Working"));
}
progress.Report(Tuple.Create(-1,"Cancelled!"));
}
我只是在懒惰并返回Tuple<int,string>
。专门的进度类在生产代码中更合适。
使用Action的优点是您不需要管理事件处理程序,并且对象是异步方法的本地对象。清理由.NET本身执行。
如果您的设备API提供真正的异步通话,则您不需要Task.Run
。这意味着您不必浪费任务在一个环路中,例如:
private async Task MeasureInBackground(CancellationToken token,IProgress<Tuple<int,string>> progress)
{
while(!token.IsCancellationRequested)
{
await Task.Delay(100, token);
int data = await GetDataFromDeviceAsync();
progress.Report(Tuple.Create(data,"Working"));
}
progress.Report(Tuple.Create(-1,"Cancelled!"));
}
大多数驱动程序使用称为完成端口的操作系统功能执行IO任务,实质上是在驱动程序完成操作时调用的回调。这样他们就不需要在等待网络,数据库或文件系统响应时阻塞。
修改强>
在上一个示例中,不再需要Task.Run
。只需使用await
即可:
await MeasureInBackground(token,progress);
答案 2 :(得分:2)
首先,async
方法可能是虚幻的,因为它们不会让你的方法神奇地异步。相反,您可以将异步方法视为状态机的设置(请参阅详细说明here),您可以通过await
调用来安排操作链。
因此,您的异步方法必须尽快执行。请勿在此类设置方法中执行任何阻止操作。如果您要在异步方法中执行阻止操作,请通过await Task.Run(() => MyLongOperation());
调用进行计划。
例如,这将立即返回:
private async Task Task1()
{
await Task.Run(() =>
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(500);
ChangeParameter(0);
Thread.Sleep(1000);
ChangeParameter(10);
Thread.Sleep(500);
ChangeParameter(0);
}
}
}
一句小话:其他人可能会建议使用Task.Delay
代替Thread.Sleep
。我会说只有当它是状态机配置的一部分时才使用Task.Delay
。但是,如果将延迟用作长期操作的一部分,而您不想分开,则只需停留在Thread.Sleep
。
最后,这部分的评论:
private async void button1_Click(object sender, EventArgs e)
{
await Task1();
await Task2();
}
这会将您的任务配置为彼此执行。如果你想并行执行它们,可以这样做:
private async void button1_Click(object sender, EventArgs e)
{
Task t1 = Task1();
Task t2 = Task2();
await Task.WhenAll(new[] { t1, t2 });
}
编辑:长期任务的额外注释:默认情况下,Task.Run
执行池线程上的任务。调度太多并行和持久的任务可能会导致饥饿,整个应用程序可能会长时间冻结。因此,对于持久操作,您可能希望将Task.Factory.StartNew
与TaskCreationOptions.LongRunning
选项一起使用,而不是Task.Run
。
// await Task.Run(() => LooongOperation(), token);
await Task.Factory.StartNew(() => LooongOperation(), token, TaskCreationOptions.LongRunning, TaskScheduler.Default);