Async Await to Keep Event Firing

2019-05-02 13:09发布

I have a question about async\await in a C# .NET app. I'm actually trying to solve this problem in a Kinect based application but to help me illustrate, I've crafted this analogous example:

Imagine that we have a Timer, called timer1 which has a Timer1_Tick event set up. Now, the only action I take on that event is to update the UI with the current date time.

 private void Timer1_Tick(object sender, EventArgs e)
    {
        txtTimerValue.Text = DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture);
    }

This is simple enough, my UI updates every few hundredths of seconds and I can happily watch time go by.

Now imagine that I also want to also calculate the first 500 prime numbers in the same method like so:

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

This is when I experience the problem. The intensive method that calculates the prime numbers blocks the event handler from being run - hence my timer text box now only updates every 30 seconds or so.

My question is, how can I resolve this while observing the following rules:

  • I need my UI timer textbox to be as smooth as it was before, probably by pushing the intensive prime number calculation to a different thread. I guess, this would enable the event handler to run as frequently as before because the blocking statement is no longer there.
  • Each time the prime number calculation function finishes, it's result to be written to the screen (using my PrintPrimeNumbersToScreen() function) and it should be immediately started again, just in case those prime numbers change of course.

I have tried to do some things with async/await and making my prime number calculation function return a Task> but haven't managed to resolve my problem. The await call in the Timer1_Tick event still seems to block, preventing further execution of the handler.

Any help would be gladly appreciated - I'm very good at accepting correct answers :)

Update: I am very grateful to @sstan who was able to provide a neat solution to this problem. However, I'm having trouble applying this to my real Kinect-based situation. As I am a little concerned about making this question too specific, I have posted the follow up as a new question here: Kinect Frame Arrived Asynchronous

3条回答
相关推荐>>
2楼-- · 2019-05-02 13:25

May not be the best solution, but it will work. You can create 2 separate timers. Your first timer's Tick event handler only needs to deal with your txtTimerValue textbox. It can remain the way you had it originally:

private void Timer1_Tick(object sender, EventArgs e)
{
    txtTimerValue.Text = DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture);
}

For your 2nd timer's Tick event handler, define the Tick event handler like this:

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...

You'll notice that I changed your PrintPrimeNumbersToScreen() method to ConvertPrimeNumbersToString() (the rest remains the same). The reason for the change is that you really want to minimize the amount of work being done on the UI thread. So best to prepare the string from the background thread, and then just do a simple assignment to the txtPrimeAnswers textbox on the UI thread.

EDIT: Another alternative that can be used with a single timer

Here is another idea, but with a single timer. The idea here is that your Tick even handler will keep executing regularly and update your timer value textbox every time. But, if the prime number calculation is already happening in the background, the event handler will just skip that part. Otherwise, it will start the prime number calculation and will update the textbox when it's done.

// 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...
查看更多
神经病院院长
3楼-- · 2019-05-02 13:31

So you want to start a Task without waiting for the result. When the task has finished calculating it should update the UI.

First some things about async-await, later your answer

The reason that your UI isn't responsive during the long action is because you didn't declare your event handler async. The easiest way to see the result of this is by creating an event handler for a button:

synchronous - UI is blocked during execution:

private void Button1_clicked(object sender, EventArgs e)
{
    List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
    PrintPrimeNumbersToScreen(primeNumbersList);
}

asynchronous - UI is responsive during execution:

private async void Button1_clicked(object sender, EventArgs e)
{
    List<int> primeNumbersList = await Task.Run( () => WorkOutFirstNPrimeNumbers(500));
    PrintPrimeNumbersToScreen(primeNumbersList);
}

Note the differences:

  • The function is declared async
  • Instead of calling the function, a Task is started using Task.Run
  • the await statement makes sure your UI-thread returns and keeps handling all UI requests.
  • once the task is finished, the UI thread continues with the next part of the await.
  • the value of the await is the return of the WorkOutFirstNPrimeNumber function

Note:

  • normally you'll see async functions return a Task instead of void and Task<TResult> instead of TResult.
  • The await Task is a void and await Task<TResult> is a TResult.
  • To start a function as a separate task use Task.Run ( () => MyFunction(...))
  • The return of Task.Run is an awaitable Task.
  • Whenever you want to use await, you'll have to declare your function async, and thus return Task or Task<TResult>.
  • So your callers have to be async and so forth.
  • The only async function that may return void is the event handler.

Your problem: timer tick reported when calculations still busy

The problem is that your timer is faster than your calculations. What do you want if a new tick is reported when the previous calculations are not finished

  1. Start new calculations anyhow. This might lead to a lot of threads doing calculations at the same time.
  2. Ignore the tick until no calculations are busy
  3. You could also choose to let only one task do the calculations and start them as soon as they are finished. In that case the calculations run continuously

(1) Start the Task, but do not await for it.

private void Button1_clicked(object sender, EventArgs e)
{
    Task.Run ( () =>
    {    List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
        PrintPrimeNumbersToScreen(primeNumbersList);
    }); 
}

(2) ignore the tick if the task is still busy:

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) Start a task that calculates continuously

private void StartTask(CancellationToken token)
{
    Task.Run( () =>
    {
        while (!token.IsCancelRequested)
        {
            List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
            PrintPrimeNumbersToScreen(primeNumbersList);
        }
     })
 }
查看更多
The star\"
4楼-- · 2019-05-02 13:40

You should avoid using async/await (despite how good they are) because Microsoft's Reactive Framework (Rx) - NuGet either "Rx-WinForms" or "Rx-WPF" - is a far better approach.

This is the code you would need for a Windows Forms solution:

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

That's it. Very simple. It all happens on background threads before being marshalled back to the UI.

The above should be fairly self explanatory, but yell out if you need any further explanation.

查看更多
登录 后发表回答