异步等待保持事件触发

本文关键字:事件 等待 异步 | 更新日期: 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的异步函数是事件处理程序。

你的问题:计时器滴答报告时,计算仍然繁忙

问题是你的计时器比你的计算快。如果在之前的计算未完成时报告了一个新的刻度,您想要什么

    无论如何,开始新的计算。这可能会导致很多线程同时进行计算。
  1. 忽略tick直到没有计算繁忙
  2. 您还可以选择只让一个任务进行计算,并在完成后立即启动它们。在这种情况下,计算连续运行

(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);
        }
     })
 }