28

I have the following code in a WinForms application with one button and one label:

using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            await Run();
        }

        private async Task Run()
        {
            await Task.Run(async () => {
                await File.AppendText("temp.dat").WriteAsync("a");
                label1.Text = "test";
            });    
        }
    }
}

This is a simplified version of the real application I'm working on. I was under the impression that by using async/await in my Task.Run I could set the label1.Text property. However, when running this code I get the error that I'm not on the UI thread and I can't access the control.

Why can't I access the label control?

Wouter de Kort
  • 39,090
  • 12
  • 84
  • 103

6 Answers6

35

When you use Task.Run(), you're saing that you don't want the code to run on the current context, so that's exactly what happens.

But there is no need to use Task.Run() in your code. Correctly written async methods won't block the current thread, so you can use them from the UI thread directly. If you do that, await will make sure the method resumes back on the UI thread.

This means that if you write your code like this, it will work:

private async void button1_Click(object sender, EventArgs e)
{
    await Run();
}

private async Task Run()
{
    await File.AppendText("temp.dat").WriteAsync("a");
    label1.Text = "test";
}
svick
  • 236,525
  • 50
  • 385
  • 514
  • 3
    You should not us async void methods from the GUI synchronization context. C.f. http://msdn.microsoft.com/en-us/magazine/jj991977.aspx —"GUI and ASP.NET applications have a SynchronizationContext that permits only one chunk of code to run at a time. When the await completes, it attempts to execute the remainder of the async method within the captured context. But that context already has a thread in it, which is (synchronously) waiting for the async method to complete. They’re each waiting for the other, causing a deadlock." – Daniel Aug 07 '14 at 21:58
  • 16
    @Daniel That quote is about synchronous waiting (e.g. `Wait()`) on asynchronous code, and I'm certainly not suggesting that. From the same article: “Void-returning async methods have a specific purpose: to make asynchronous event handlers possible.” And that's exactly what I'm doing here. – svick Aug 07 '14 at 22:48
  • 2
    It's worth noting that any exception that occurs in the `private async Task Run()` method that is not handled, will "bubble" up to the asynchronous event handler itself. – Jaans Nov 14 '14 at 04:21
  • Incidentally, what DOES happen when an async void throws an exception? Does the exception just vanish into oblivion and the thread ends uncleanly and just goes back into the pool or does it bring down the entire show? – KatDevsGames Jan 18 '15 at 04:31
  • @JoshuaPech It's rethrown on the synchronization context. This usually means it will crash the application. – svick Jan 18 '15 at 12:09
  • Imagine that File.AppendText("temp.dat").WriteAsync("a") wants to access a textBox. what happens then? – X.Otano Oct 04 '18 at 07:40
20

Try this:

replace

label1.Text = "test";

with

SetLabel1Text("test");

and add the following to your class:

private void SetLabel1Text(string text)
{
  if (InvokeRequired)
  {
    Invoke((Action<string>)SetLabel1Text, text);
    return;
  }
  label1.Text = text;
}

The InvokeRequired returns true if you are NOT on the UI thread. The Invoke() method takes the delegate and parameters, switches to the UI thread and then calls the method recursively. You return after the Invoke() call because the method has already been called recursively prior to the Invoke() returning. If you happen to be on the UI thread when the method is called, the InvokeRequired is false and the assignment is performed directly.

Metro
  • 1,464
  • 14
  • 24
  • 1
    this is old fashion code. We are talking about async await – hannes neukermans Mar 21 '18 at 15:30
  • 2
    Old fashion code or not, this is the way I ensure that I am on the UI thread before updating a control. async await doesn't really change the problem. – Metro Mar 21 '18 at 20:44
  • 1
    old fashion, show me a better alternative for winforms, by the way this works well with async/await and task parallel library. – Raiden Core Jan 20 '21 at 19:11
  • 1
    Give this man a raise, it solved my issue. My app was crashing with just await / async. This look like a safe way to update the controls from an async method. @hannesneukermans do you have something better? – Slamit Oct 28 '21 at 11:44
  • 1
    Some 7 years later =) Yes, async/await won't help with errors regarding doing UI updates on other threads than the UI thread. You need to hand over the executing to the UI thread, and Invoke is one such way to do it. – Ted Nov 03 '21 at 20:39
16

Try this

private async Task Run()
{
    await Task.Run(async () => {
       await File.AppendText("temp.dat").WriteAsync("a");
       });
    label1.Text = "test";
}

Or

private async Task Run()
{
    await File.AppendText("temp.dat").WriteAsync("a");        
    label1.Text = "test";
}

Or

private async Task Run()
{
    var task = Task.Run(async () => {
       await File.AppendText("temp.dat").WriteAsync("a");
       });
    var continuation = task.ContinueWith(antecedent=> label1.Text = "test",TaskScheduler.FromCurrentSynchronizationContext());
    await task;//I think await here is redundant        
}

async/await doesn't guarantee that it will run in UI thread. await will capture the current SynchronizationContext and continues execution with the captured context once the task completed.

So in your case you have a nested await which is inside Task.Run hence second await will capture the context which is not going to be UiSynchronizationContext because it is being executed by WorkerThread from ThreadPool.

Does this answers your question?

Sriram Sakthivel
  • 72,067
  • 7
  • 111
  • 189
6

Why do you use Task.Run? that start a new worker thread (cpu bound), and it causes your problem.

you should probably just do that:

    private async Task Run()
    {
        await File.AppendText("temp.dat").WriteAsync("a");
        label1.Text = "test";    
    }

await ensure you will continue on the same context except if you use .ConfigureAwait(false);

Persi
  • 335
  • 1
  • 11
1

Because it's on a different thread and cross-thread calls aren't allowed.

You will need to pass on the "context" to the thread you are starting. See an example here: http://reedcopsey.com/2009/11/17/synchronizing-net-4-tasks-with-the-ui-thread/

Gerrie Schenck
  • 22,148
  • 20
  • 68
  • 95
-1

I am going to give you my latest answer that I have given for async understanding.

The solution is as you know that when you are calling async method you need to run as a task.

Here is a quick console app code that you can use for your reference, it will make it easy for you to understand the concept.

using System;
using System.Threading;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Starting Send Mail Async Task");
        Task task = new Task(SendMessage);
        task.Start();
        Console.WriteLine("Update Database");
        UpdateDatabase();

        while (true)
        {
            // dummy wait for background send mail.
            if (task.Status == TaskStatus.RanToCompletion)
            {
                break;
            }
        }

    }

    public static async void SendMessage()
    {
        // Calls to TaskOfTResult_MethodAsync
        Task<bool> returnedTaskTResult = MailSenderAsync();
        bool result = await returnedTaskTResult;

        if (result)
        {
            UpdateDatabase();
        }

        Console.WriteLine("Mail Sent!");
    }

    private static void UpdateDatabase()
    {
        for (var i = 1; i < 1000; i++) ;
        Console.WriteLine("Database Updated!");
    }

    private static async Task<bool> MailSenderAsync()
    {
        Console.WriteLine("Send Mail Start.");
        for (var i = 1; i < 1000000000; i++) ;
        return true;
    }
}

Here I am trying to initiate task called send mail. Interim I want to update database, while the background is performing send mail task.

Once the database update has happened, it is waiting for the send mail task to be completed. However, with this approach it is quite clear that I can run task at the background and still proceed with original (main) thread.

codebased
  • 6,945
  • 9
  • 50
  • 84
  • 2
    I'd never down-vote someone who takes the time to write an answer, but I do think `while (true)` might hog the CPU resources, making the task take much longer than it would otherwise. –  Jan 13 '15 at 19:02
  • This doesn't update the UI thread as an ongoing step. The OP isn't asking how to wait until everything is done, they want to update a label while the Task is running, not after. – Tom Padilla May 18 '17 at 18:45