简体   繁体   中英

Wait for Task on UI Thread without producing a deadlock

Keep in mind: I'm on .NET 4.0 and can't use the async/await pattern nor .ConfigureAwait .

I'm currently trying to keep the UI responsive while executing a longer-running operation, primary to be able to cancel the operation if needed.

Therefore, I've used Task.Factory.StartNew to start a new task on the UI Thread and Wait to wait for its completion.

It is important that the operation finished before I continue, that's why I used Wait to wait for its completion. However, this creates a deadlock on the UI thread.

Complete Code:

// currently on the UI thread
Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(x =>
{
    // simple output that I'm done
}).Wait(); // -> deadlock. can't remove it, otherwise the app would continue

Calling that code looks like a normal function call

private void Run(){
    DoStuff();
    DoMoreStuff(); // it's important that DoStuff has finished, that's why removing .Wait won't work
}

private void DoStuff()
{
    Task.Factory.StartNew(() => LongerOperation())
    .ContinueWith(x =>
    {
        // simple output that I'm done
    }).Wait();
}

How can I wait for the task to complete without creating a deadlock on the UI thread? Other answers suggest to use the async/await pattern but I'm not able to use that.

For starters, you probaly aren't working on .NET 4.0 because .NET 4.x runtimes are binary replacements. Installing a .NET 4.5+ application or Windows Update patch means all applications now work on .NET 4.5 and later. Your development machine works on .NET 4.5+ at least, which means you already develop on a different runtime than the one you intend to target.

The only exception is if you target unsupported OS versions like Windows XP and Windows Server 2003, which never got .NET 4.5 support.

In any case, using Microsoft.Bcl.Async is a viable option, as you've already accepted far greater risks, like running on 4.5 while targetting 4.0, or running on an unsupported OS that doesn't even have TLS1.2 support, a minimum requirement for most services nowadays, including Google, AWS, Azure, banks, payment gateways, airlines etc.

The other option is to use ContinueWith with a TaskScheduler parameter that specifies where to run the continuation. TaskScheduler.FromCurrentSynchronizationContext() specifies that the continuation will run on the original synchronization context. In a Winforms or WPF application, that's the UI thread.

Back then we used to write code like this:

Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(t =>
{
    textBox.Text=String.Format("The result is {0}",t.Result);
},TaskScheduler.FromCurrentSynchronizationContext());

Handling exceptions and chaining multiple asynchronous operations takes extra work. t.Result will throw an exception if the task failed, so you need to check Task.IsFaulted or Task.IsCancelled before you try to use its value.

There's no way to short-circuit a chain of continuations with a simple return the way you can with .NET 4.5 and async/await. You can check the IsFaulted flag and avoid updating the UI, but you can't prevent the next continuation down the chain from executing.

Task.Factory.StartNew(() => LongerOperation())
    .ContinueWith(t=>
    {
        if (!t.IsFaulted)
        {
            return anotherLongOperation(t.Result);
        }
        else
        {
            //?? What do we do here?
            //Pass the buck further down.
            return Task.FromException(t.Exception);
        }
    })         
    .ContinueWith(t =>
    {
        //We probably need `Unwrap()` here. Can't remember
        var result=t.Unwrap().Result;
        textBox.Text=String.Format("The result is {0}",result);
    },TaskScheduler.FromCurrentSynchronizationContext());

To stop execution in case of failure you'll have to use the TaskContinuationOptions parameter and pass eg NotOnFaulted .

Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(t =>
    {
        textBox.Text=String.Format("The result is {0}",t.Result);
    },
    CancellationToken.None,
    TaskContinuationOptions.NotOnFaulted,
    TaskScheduler.FromCurrentSynchronizationContext());

What do you do if you want to end execution due to some business rule? You'll have to return something that all subsequent steps will need to forward to the end of the chain.

In the end, using Microsoft.Bcl.Async can result in far simpler code

Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(t=>
{
    if (!t.IsFaulted)
    {
        var num=anotherLongOperation(t.Result);
        if (num<0)
        {
            //Now what?
            //Let's return a "magic" value
            return Task.FromResult(null);
        }
    }
    else
    {
        //?? What do we do here?
        //Pass the buck further down.
        return Task.FromException(t.Exception);
    }
})         
.ContinueWith(t =>
{
    //This may need t.Result.Result
    if (!t.IsFaulted && t.Result!=null)
    {
        textBox.Text=String.Format("The result is {0}",t.Result);
    }
},TaskScheduler.FromCurrentSynchronizationContext());

I'm not sure if continuations farther down need Unwrap() or not. They probably do, as the second step returns a Task<T> . Forgetting this can lead to hard-to-debug problems too.

Using Microsoft.Bcl.Async results in cleaner, simpler code that's far easier to get right :

public async Task DoRun()
{
    try
    {
        var x=await longOperation();
        var num=await anotherLongOperation(x);
        if(num<0)
        {
            return;
        }
        textBox.Text=String.Format("The result is {0}",num);
    }
    catch(Exception exc)
   {
      //Do something about it
   }
}

Update

As Stephen Cleary noted, BLC Async only worked with Visual Studio 2012. Which is also no longer supported and probably only available as a download for MSDN Subscribers.

Back in 2010 the Parallel team release some extensions and samples to make parallel and asynchronous processing easier, the Parallel Extensions Extras . That library included externsions like Then that made chaining and error handling a lot easier.

Most of these features are included in .NET itself since 4.5 so the samples and library weren't updated since 2011. Any NuGet packages or Github repos that claim to be the Parallel Extras simply clone and repackage this download.

That library used to be documented by a series of articles by Stephen Toub but Microsoft changed its blogging engine recently and the URLs of old migrated blog posts no longer work.

Since neither .NET 4.0 nor the Parallel Extras are supported, the links weren't fixed. Those articles are still available under a different URL

"No longer supported" means the docs, libraries and knowledge may be lost too. Some of the people that worked with tasks 7 years ago may remember the libraries and projects used back then, but 7 years is far too long.

Try MSDN: Backgroundworker . You can cancel your task by doing backgroundworkerObject.CancelAsync(); if you have done backgroundWorkerObject.WorkerSupportsCancellation = true; in the form's constructor. This executes your long operation in a seperate thread without causing a deadlock in your UI.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM