Grimson
Grimson

Reputation: 572

C# Async Function returning prematurely

I'm trying to wait for an asynchronous function to complete so that I can populate a ListView in my UI thread.

Here's the code

    public Form1()
    {
        InitializeComponent();

        Task t = new Task(Repopulate);
        t.Start();
        // some other work here
        t.Wait(); //completes prematurely

    }

    async void Repopulate()
    {           
        var query = ParseObject.GetQuery("Offer");
        IEnumerable<ParseObject> results = await query.FindAsync();

        if (TitleList == null)
            TitleList = new List<string>();
        foreach (ParseObject po in results)
            TitleList.Add(po.Get<string>("title"));
    }

TitleList = null in Form1() because Repopulate() hasn't been completed yet. Therefore I used Wait(). However, wait returns before function is even completed.

What am I doing wrong here?

Upvotes: 0

Views: 1348

Answers (1)

Douglas
Douglas

Reputation: 54877

You need to change the return type of your Repopulate method to return a task representing the asynchronous operation.

Also, you shouldn't perform asynchronous operations from your form constructor, since calling Task.Wait will cause your UI thread to block (and your application to appear unresponsive). Rather, subscribe to its Form.Load method, and perform the asynchronous operations there, using the await keyword to keep the event handler asynchronous. If you don't want the user to interact with the form until the asynchronous operation completes, then disable the form within the constructor and re-enable it at the end of the Load handler.

private async void Form1_Load(object sender, EventArgs e)
{
    Task t = Repopulate();
    // If you want to run its synchronous part on the thread pool:
    // Task t = Task.Run(() => Repopulate());

    // some other work here
    await t;
}

async Task Repopulate()
{           
    var query = ParseObject.GetQuery("Offer");
    IEnumerable<ParseObject> results = await query.FindAsync();

    if (TitleList == null)
        TitleList = new List<string>();
    foreach (ParseObject po in results)
        TitleList.Add(po.Get<string>("title"));
}

Update: For the benefit of future readers, I'm reworking my comments into the answer:

Task.Wait causes the calling thread to block until the task completes. Form constructors and event handlers, by their nature, run on the UI thread, so calling Wait within them will cause the UI thread to block. The await keyword, on the other hand, will cause the current method to relinquish control back to the caller – in the case of event handlers, this would allow the UI thread to continue processing events. The awaiting method (event handler) would then resume execution on the UI thread after the task completes.

Task.Wait will always block the calling thread, whether it's called from the constructor or the event handler, so one should avoid using it, especially when running on the UI thread. C# 5 introduced the async and await keywords to this end; however, they're only supported on methods, not constructors. This restriction underlies the main reason why you need to move your initialization code from the form constructor to an async event handler.

As to the reason why Task.Wait() returns prematurely: In your original code, task t represents the execution of the Task that you instantiated in your form constructor. This task runs Repopulate; however, the said method will return as soon as it encounters the first await statement, and execute the rest of its logic in a fire-and-forget fashion. This is the danger of using async void – you won't know when the asynchronous method has completed executing. (For this reason, async void should only be used for event handlers.) In other words, t.Wait() returns as soon as Repopulate hits its first await.

By changing the signature of Repopulate to async Task, you are now getting another task that represents the completion of its asynchronous execution, including the query.FindAsync() asynchronous call and the processing that succeeds it. When Task.Run is passed an asynchronous operation (Func<Task>) as argument, its returned task will wait for (unwrap) the inner task. This is why Task.Run should be used instead of Task.Start or Task.Factory.StartNew.

Upvotes: 7

Related Questions