异步等待保持事件触发
本文关键字:事件 等待 异步 | 更新日期: 2023-09-27 18:08:05
我有一个关于c# . net应用程序中的async'await的问题。我实际上试图在基于Kinect的应用程序中解决这个问题,但为了帮助我说明,我制作了这个类似的例子:
假设我们有一个名为timer1的计时器,它设置了一个Timer1_Tick事件。现在,我对该事件采取的唯一操作是使用当前日期时间更新UI。
private void Timer1_Tick(object sender, EventArgs e)
{
txtTimerValue.Text = DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture);
}
这很简单,我的UI每百分之几秒更新一次,我可以高兴地看着时间流逝。
现在假设我还想用同样的方法计算前500个素数,如下所示:
private void Timer1_Tick(object sender, EventArgs e)
{
txtTimerValue.Text = DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture);
List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
PrintPrimeNumbersToScreen(primeNumbersList);
}
private List<int> WorkOutFirstNPrimeNumbers(int n)
{
List<int> primeNumbersList = new List<int>();
txtPrimeAnswers.Clear();
int counter = 1;
while (primeNumbersList.Count < n)
{
if (DetermineIfPrime(counter))
{
primeNumbersList.Add(counter);
}
counter++;
}
return primeNumbersList;
}
private bool DetermineIfPrime(int n)
{
for (int i = 2; i < n; i++)
{
if (n % i == 0)
{
return false;
}
}
return true;
}
private void PrintPrimeNumbersToScreen(List<int> primeNumbersList)
{
foreach (int primeNumber in primeNumbersList)
{
txtPrimeAnswers.Text += String.Format("The value {0} is prime 'r'n", primeNumber);
}
}
这就是我遇到问题的时候。计算素数的密集方法阻止了事件处理程序的运行-因此我的计时器文本框现在每30秒左右更新一次。
我的问题是,如何在遵守以下规则的同时解决这个问题:
- 我需要我的UI计时器文本框像以前一样平滑,可能是通过将密集的素数计算推到不同的线程。我猜,这将使事件处理程序像以前一样频繁地运行,因为阻塞语句不再存在。
- 每次素数计算函数完成时,它的结果将被写入屏幕(使用我的PrintPrimeNumbersToScreen()函数),并且它应该立即再次启动,以防这些素数改变。
我试图做一些事情与async/await和使我的素数计算函数返回一个任务>,但没有设法解决我的问题。Timer1_Tick事件中的await调用似乎仍然阻塞,阻止了处理程序的进一步执行。
任何帮助都会很感激-我很擅长接受正确的答案:)
更新:我非常感谢@sstan,他能够为这个问题提供一个简洁的解决方案。然而,我却很难将其应用到基于kinect的实际情况中。因为我有点担心这个问题太过具体,所以我在这里发布了一个新问题:Kinect帧到达异步
可能不是最好的解决方案,但它会起作用。您可以创建2个单独的计时器。您的第一个计时器的Tick
事件处理程序只需要处理您的txtTimerValue
文本框。它可以保持原来的样子:
private void Timer1_Tick(object sender, EventArgs e)
{
txtTimerValue.Text = DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture);
}
对于第二个定时器的Tick
事件处理程序,像这样定义Tick
事件处理程序:
private async void Timer2_Tick(object sender, EventArgs e)
{
timer2.Stop(); // this is needed so the timer stops raising Tick events while this one is being awaited.
txtPrimeAnswers.Text = await Task.Run(() => {
List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
return ConvertPrimeNumbersToString(primeNumbersList);
});
timer2.Start(); // ok, now we can keep ticking.
}
private string ConvertPrimeNumbersToString(List<int> primeNumbersList)
{
var primeNumberString = new StringBuilder();
foreach (int primeNumber in primeNumbersList)
{
primeNumberString.AppendFormat("The value {0} is prime 'r'n", primeNumber);
}
return primeNumberString.ToString();
}
// the rest of your methods stay the same...
您会注意到我将PrintPrimeNumbersToScreen()
方法更改为ConvertPrimeNumbersToString()
(其余部分保持不变)。更改的原因是您确实希望最小化UI线程上完成的工作量。所以最好从后台线程准备字符串,然后在UI线程上对txtPrimeAnswers
文本框做一个简单的赋值。
EDIT:可与单个计时器一起使用的另一种选择
这是另一个想法,但使用单个计时器。这里的想法是,您的Tick
甚至处理程序将保持定期执行,并每次更新您的计时器值文本框。但是,如果素数计算已经在后台进行,则事件处理程序将跳过这一部分。否则,它将开始质数计算,并在计算完成后更新文本框。
// global variable that is only read/written from UI thread, so no locking is necessary.
private bool isCalculatingPrimeNumbers = false;
private async void Timer1_Tick(object sender, EventArgs e)
{
txtTimerValue.Text = DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture);
if (!this.isCalculatingPrimeNumbers)
{
this.isCalculatingPrimeNumbers = true;
try
{
txtPrimeAnswers.Text = await Task.Run(() => {
List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
return ConvertPrimeNumbersToString(primeNumbersList);
});
}
finally
{
this.isCalculatingPrimeNumbers = false;
}
}
}
private string ConvertPrimeNumbersToString(List<int> primeNumbersList)
{
var primeNumberString = new StringBuilder();
foreach (int primeNumber in primeNumbersList)
{
primeNumberString.AppendFormat("The value {0} is prime 'r'n", primeNumber);
}
return primeNumberString.ToString();
}
// the rest of your methods stay the same...
你应该避免使用async/await(尽管它们有多好),因为微软的响应式框架(Rx) - NuGet"Rx- winforms"或"Rx- wpf"-是一个更好的方法。
这是你需要的Windows窗体解决方案的代码:
private void Form1_Load(object sender, EventArgs e)
{
Observable
.Interval(TimeSpan.FromSeconds(0.2))
.Select(x => DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture))
.ObserveOn(this)
.Subscribe(x => txtTimerValue.Text = x);
txtPrimeAnswers.Text = "";
Observable
.Interval(TimeSpan.FromSeconds(0.2))
.Select(n => (int)n + 1)
.Where(n => DetermineIfPrime(n))
.Select(n => String.Format("The value {0} is prime'r'n", n))
.Take(500)
.ObserveOn(this)
.Subscribe(x => txtPrimeAnswers.Text += x);
}
就是这样。非常简单。这一切都发生在后台线程上,然后被编组回UI。
以上内容应该是不言自明的,但如果需要进一步的解释,请大声说出
所以您想要在不等待结果的情况下启动Task。当任务完成计算后,它应该更新UI。
首先是关于async-await的一些事情,然后是您的答案
你的UI在长动作期间没有响应的原因是因为你没有声明你的事件处理程序async。查看结果的最简单方法是为按钮创建一个事件处理程序:
synchronous - UI在执行过程中被阻塞:
private void Button1_clicked(object sender, EventArgs e)
{
List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
PrintPrimeNumbersToScreen(primeNumbersList);
}
异步- UI在执行期间响应:
private async void Button1_clicked(object sender, EventArgs e)
{
List<int> primeNumbersList = await Task.Run( () => WorkOutFirstNPrimeNumbers(500));
PrintPrimeNumbersToScreen(primeNumbersList);
}
注意区别:
- 函数声明为async
- 不是调用函数,而是使用Task来启动Task。运行
- await语句确保UI线程返回并继续处理所有UI请求。
- 一旦任务完成,UI线程继续等待的下一部分。
- await的值是workoutfirstnprimennumber函数的返回值
注意:
- 通常你会看到异步函数返回Task而不是void和Task
<TResult
>而不是result . - 等待任务是一个空的等待任务
<TResult
>是一个结果。 要将函数作为单独的任务启动,请使用task。Run (() =>MyFunction(…)) - 任务返回。
- 当你想要使用await时,你必须声明你的函数是async的,因此返回Task或Task
<TResult
> - 所以你的调用者必须是异步的等等。 唯一可能返回void的异步函数是事件处理程序。
你的问题:计时器滴答报告时,计算仍然繁忙
问题是你的计时器比你的计算快。如果在之前的计算未完成时报告了一个新的刻度,您想要什么
- 无论如何,开始新的计算。这可能会导致很多线程同时进行计算。
- 忽略tick直到没有计算繁忙
- 您还可以选择只让一个任务进行计算,并在完成后立即启动它们。在这种情况下,计算连续运行
(1)启动任务,但不等待。
private void Button1_clicked(object sender, EventArgs e)
{
Task.Run ( () =>
{ List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
PrintPrimeNumbersToScreen(primeNumbersList);
});
}
(2)如果任务仍在忙,忽略勾号:
Task primeCalculationTask = null;
private void Button1_clicked(object sender, EventArgs e)
{
if (primeCalculationTask == null || primeCalculationTask.IsCompleted)
{ // previous task finished. Stat a new one
Task.Run ( () =>
{ List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
PrintPrimeNumbersToScreen(primeNumbersList);
});
}
}
(3)启动一个连续计算的任务
private void StartTask(CancellationToken token)
{
Task.Run( () =>
{
while (!token.IsCancelRequested)
{
List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
PrintPrimeNumbersToScreen(primeNumbersList);
}
})
}