Search code examples
c#.nettaskui-threadcontinuewith

Update UI with chains of ContinueWiths


I try to use multiple ContinueWiths to update the UI. So i expect that each ContinueWith Task will update the UI separately, but what really happens, they wait for each other and update the UI at once. So even thou each ContinueWith runs on time, but the UI only updates when the last one is done.

Thread.Sleep represents Work in this context.

ANSWER: StartNew is Dangerous by Stephen Cleary. The last part of the blog post pretty much using the same example as mine.

I edited the post answering my own question, ~side by side.

EDIT: In Test1() StartNew will run on the thread pool since there isn't any specific TaskScheduler set. So 'Work' will run on the thread pool, but 'More Work' and 'stop' will run on the GUI thread, since there is a set context.

exp:

    private void Test1()
    {
        var context = TaskScheduler.FromCurrentSynchronizationContext();

        listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Start");

        Task task = Task.Factory.StartNew(() =>
            {
                Thread.Sleep(1000);
                Task.Factory.StartNew(() =>
                {
                    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Work");
                }, CancellationToken.None, TaskCreationOptions.None, context);
            })
            .ContinueWith(_ =>
            {
                Thread.Sleep(2000);
                listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - More Work");
            }, context)
            .ContinueWith(_ =>
                {
                    Thread.Sleep(1000);
                    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - stop");
                }, context);
    }

EDIT:But, if 'Work' runs on the thread pool, why there is a delay in the result? Because i was wrong. What you see down below is only true, when i run the test app the second or 'n'th time.

If you run this the following happens:

13:32:00 - Start
--- 4 seconds later ---
13:32:01 - Work
13:32:03 - More Work
13:32:04 - stop

If i run Debug the first time, it looks like this:

13:32:00 - Start
--- 1 second later ---
13:32:01 - Work
--- 3 seconds later ---
13:32:03 - More Work
13:32:04 - stop

EDIT: I guess, if i run Test1() the first time, 'Work' truly runs on the thread pool. But on the second time TaskScheduler.FromCurrentSynchronizationContext() will apply to 'Work' as well, so it will runs on the GUI thread. Those Thread.Sleeps are truly runing on the GUI thread, i can't move the Form.

So my first question is, why the UI is not updating separately? Because this is what i expect.

If i start a separate Task from each ContinueWith, i get the desired result (separate UI update), but this just looks wrong/ugly/complicated and i have a feeling that i'm not using ContinueWith correctly (for UI update).

EDIT: The reason why this works is, because I'm not using the 'context' with the ContinueWiths, so they will run on the Thread pool, not locking up the UI. The newly created Tasks will run on the UI, because that's they set context.

    private void Test2()
    {
        var context = TaskScheduler.FromCurrentSynchronizationContext();

        listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Start");

        Task task = Task.Factory.StartNew(() =>
            {
                Thread.Sleep(1000);
                Task.Factory.StartNew(() =>
                {
                    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Short Work");
                }, Task.Factory.CancellationToken, TaskCreationOptions.None, context);

            })
            .ContinueWith(_ =>
            {
                Thread.Sleep(2000);
                Task.Factory.StartNew(() =>
                {
                    listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Long Work");
                }, Task.Factory.CancellationToken, TaskCreationOptions.None, context);
            })
            .ContinueWith(_ =>
                {
                    Thread.Sleep(1000);
                    Task.Factory.StartNew(() =>
                    {
                        listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - stop");
                    }, Task.Factory.CancellationToken, TaskCreationOptions.None, context);
                });
    }

Result:

13:32:00 - Start
--- 1 second later ---
13:32:01 - Work
--- 2 seconds later ---
13:32:03 - More Work
--- 1 second later ---
13:32:04 - stop

EDIT: tl;dr: be careful with StartNew and TaskScheduler.FromCurrentSynchronizationContext();


Solution

  • The correct way is to use async/await which will always make sure that the context continues on the UI thread after the task returns even if that task is executed on another thread.

    private async Task Test1()
    {
    
        listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Start");
    
        await Task.Run(HardWorkTakesLongTime);
    
        listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - Work");
    
        await Task.Run(HardWorkTakesLongTime);
    
        listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - More Work");
    
        await Task.Run(HardWorkTakesLongTime);
    
        listBox1.Items.Add($"{DateTime.Now:HH:mm:ss} - stop");
    }
    
    void HardWorkTakesLongTime(){
        Thread.Sleep(2000);
    }
    
    
    // From a button click event or something like it
    btn_Click(){
    
        Test1();
    }