Handling long operations with cancel and progress in C# with async
In a previous post, I demonstrated a convenient way to make a wait cursor show up during “reasonably” long operations (up to a few seconds). For anything longer than that, though, a wait cursor won’t cut it — for one thing, your UI will become unresponsive. For another, there is no way (other than killing your app) for the user to stop a long process.
There are tons of different ways to provide feedback and cancel options, but until .NET 4.5/4.6, they were all a little bit kludgy. Now, thanks to async handling, it is possible to provide progress, keep your app responsive, and to do it in a way that is quite elegant.
There is quite a lot to async handling, but I wanted to provide as simple an example as I could for offloading work into another thread in such a way as to show progress and allow canceling. Here is what my application looks like:
Not much too it – you click the start button to start the process running, progress is shown until you either get a completed message, or you hit the cancel button (which is the same button, re-purposed). The “work” being done is just a simple loop with a sleep:
for (int i = 0; i < 100; i++) { Thread.Sleep(200); }
The reason for the loop and the smaller sleep duration (rather than just sleeping for 20000 milliseconds) is so that we can easily report progress by updating the progress bar, and also so we can check to see if the user has hit the cancel button. This will be more obvious in a bit. Almost all of the handling takes place in the button click handler. I’ll show the full code first, then break it down.
private bool m_running = false; CancellationTokenSource m_cancelTokenSource = null; private async void button_Click(object sender, EventArgs e) { if (!m_running) { m_running = true; statusLabel.Text = "Working"; button.Text = "Cancel"; Progress<int> prog = new Progress<int>(SetProgress); m_cancelTokenSource = new CancellationTokenSource(); try { await SlowProcess(prog, m_cancelTokenSource.Token); statusLabel.Text = "Done"; } catch (OperationCanceledException) { statusLabel.Text = "Canceled"; } finally { button.Text = "Start"; m_running = false; m_cancelTokenSource = null; } } else { statusLabel.Text = "Waiting to cancel..."; m_cancelTokenSource.Cancel(); } }
Some of the code here is related to handling the button and label text, making it show the appropriate message at the appropriate time. There is also an m_running flag that is just used to handle the button switch from a “Start” button to a “Cancel” button and back again. If we want to boil the code down to just the minimum required to cause the operation to be launched as an async operation (meaning that the work will happen on another thread), we really just need this:
private async void button_Click(object sender, EventArgs e) { statusLabel.Text = "Working"; await SlowProcess(); statusLabel.Text = "Done"; }
We technically don’t need to update the label either, but it will make the explanations simpler. There are two really important things going on with this code. First is the presence of the “async” keyword in the method signature. This tells .NET that this method is going to make some sort of asynchronous call. Second is the “await” in front of our call to SlowProcess().
If SlowProcess() was just a regular method (it’s not, but we’ll come back to that), you could easily imagine that this code would work fine without the async/await calls. The label would change to Working, the work would be done, then the label would be changed to Done. However, our UI would be completely locked during the SlowProcess() handling because the system would not get any cycles.
By adding await, we are instructing the system to launch the work in SlowProcess() on another thread, which means that the main thread will not be locked. Prior to .NET 4.5, you might have done this by explicitly launching the work on another thread. Something like:
ThreadPool.QueueUserWorkItem(SlowProcess);
But there is something strange going on here — if you just launched a new thread, once the item was queued, execution would continue on to the next line of code. The label would change to “Working”, then almost immediately change to “Done”, because launching the work on a new thread takes almost no time. Of course, nothing is really done at this point — the work is going on in another thread. With the old mechanisms, that thread would have to signal back to the main thread when it was really done, and it couldn’t just update the UI because you would get a nasty cross-thread error (you can’t update UI elements created from one thread in another thread).
This is where the magic of async comes in. Because this is an async method, the compiler effectively breaks up the code for you. When the await is hit, the process is launched, but then control is returned from the method — as if there were a “return” statement right after the thread launch. But, when the processing in the other thread is complete, execution is returned back to this statement, and execution continues on to the next statement! The steps look something like this:
- User clicks on the button, causing the button_Click method to be called.
- Label is changed to read “Working”.
- Thread is launched to execute the SlowProcess() code.
- Control is returned from the button_Click method, meaning that processing can continue.
- At some point in the future, SlowProcess() completes.
- Call is made back into the button_Click method, immediately after the async call.
- Label is changed to read “Done”.
In the example above, SlowProcess() just does some work, but it could have returned a value as well:
int result = await SlowProcess(prog, m_cancelTokenSource.Token);
There are also ways in which we could have launched work on multiple threads simultaneously, and the code would have auto-magically continued only when all of the work was completed. That is beyond the scope of this article, but the power of async is really impressive, and we are just scratching the surface right now.
Progress
Let’s get back to some of the other things that we want to handle, starting with progress. If you recall, in our original code, we created a Progress object, which we then passed to the SlowProcess() method:
Progress<int> prog = new Progress<int>(SetProgress); await SlowProcess(prog, m_cancelTokenSource.Token);
Progress is a system class that implements the IProgress<T> interface. In this case, we want to update progress with an integer, so we are using int for our T, but it could be anything. SetProgress is the name of the method we want to call when progress needs to be updated, and it looks like this:
private void SetProgress(int value) { progressBar.Value = value + 1; }
Note that this method takes an argument of type int, which matches the generic type we’ve specified for our Progress object. In the SlowProcess() code that is actually doing the work, every now and then we want to have SetProgress() be called to indicate our current status. However, we can’t just call SetProgress() because of cross-threading issues. We could manually handle that, but we don’t need to. Instead, in SlowProcess() we can just add a call like:
prog.Report(i);
Where prog is a reference to our Progress<int> object and i is the loop variable we are using that indicates how far we are in to the process. When Report() is called, a call is made to SetProgress, automatically marshaled to the correct thread. SetProgress() then just updates the progress bar’s position.
Canceling
We also have support for canceling the operation when the user hits the cancel button. There are actually two separate classes involved with cancelling:
- CancellationTokenSource
- CancellationToken
We create and hold on to a CancellationTokenSource when the operation begins:
CancellationTokenSource m_cancelTokenSource = null; private async void button_Click(object sender, EventArgs e) { m_cancelTokenSource = new CancellationTokenSource();
But we pass a cancel token to our method:
await SlowProcess(prog, m_cancelTokenSource.Token);
Note that we didn’t have to create a CancellationToken, because the CancellationTokenSource created one for us. The “Source” is the source of a cancellation request, while the “Token” is something that the code in the thread can check to see if cancellation has been requested. To signal that the operation should be cancelled, you can just call the appropriate method on the source:
m_cancelTokenSource.Cancel();
However, this really doesn’t do anything other than set a flag indicating that a cancel is desired. It is up to the process itself to check to see if a cancel was requested, with code something like:
// When convenient, check for a cancel if (ct.IsCancellationRequested) throw new OperationCanceledException(ct);
In fact, this code is so boiler-plate, that there is a single call on the CancellationToken to do it all in a single line:
ct.ThrowIfCancellationRequested();
To see this in place, here is the full action code we will be executing:
for (int i = 0; i < 100; i++) { ct.ThrowIfCancellationRequested(); prog.Report(i); Thread.Sleep(200); }
If you never check for a cancel, then you will never cancel! Note that there are some additional useful things you can do with a CancellationToken. For example, you can set up a time limit after which, if the operation is not complete, cancel should happen automatically.
The Task
We now have progress and the ability to cancel, we’ve looked at everything except the details of the work going on. Async handling makes things extremely simple, but the SlowProcess() method is not quite just a container for the code we want to run. Instead it is a method that returns a Task object. The Task is what the await call is actually dealing with. Here is the code for SlowProcess():
private Task SlowProcess(IProgress prog, CancellationToken ct) { return Task.Run(() => { for (int i = 0; i < 100; i++) { ct.ThrowIfCancellationRequested(); prog.Report(i); Thread.Sleep(200); } }, ct); }
There is a lot going on here. First of all, note that the code that we are really running is defined as an Expression, which is passed as an argument to the Task.Run() method (That is the reason for the () =>
notation). This is a useful helper method that queues up work and returns a Task. We return the Task itself from the SlowProcess() method, and the await call “awaits” on the Task. Note: we don’t really need to have the SlowProcess() method at all – we could have just put the call in place in the button_Click handler:
await Task.Run(() => { for (int i = 0; i < 100; i++) { m_cancelTokenSource.Token.ThrowIfCancellationRequested(); ((IProgress<int>)prog).Report(i); Thread.Sleep(200); } }, m_cancelTokenSource.Token);
This works in the same manner, but dumping it in the middle of the rest of the code makes the code harder to read and manage. If, however, you want to get access to the Task, you could change the code in button_Click to look like this:
Task t = SlowProcess(prog, m_cancelTokenSource.Token); await t;
This makes it a little clearer as to what is going on. There are also various situations where having access to the Task is useful. For example, if you want to wait for the result of multiple tasks before moving on.
If you cared about a return value from the expression, the code would change slightly. Here is what the SlowProcess() code would look like:
private Task<int> SlowProcess(IProgress prog, CancellationToken ct) { return Task<int>.Run(() => { for (int i = 0; i < 100; i++) { ct.ThrowIfCancellationRequested(); prog.Report(i); Thread.Sleep(200); } return 42; }, ct); }
Note that the method now returns Task<int> instead of just Task, and there is a return statement in the Expression. Hopefully with real code, the return would actually be based on some actual process, rather than hard-coded, but you get the idea!
Our calling code would also change:
Task<int> t = SlowProcess(prog, m_cancelTokenSource.Token); int result = await t;
And, of course, we could combine this is in a single line, as before:
int result = await SlowProcess(prog, m_cancelTokenSource.Token);
If we didn’t need access to the Task.
If you run this code in the debugger, and you click the cancel button, the debugger will stop on the line
ct.ThrowIfCancellationRequested()
and give you an “OperationCanceledException was unhandled by user code” popup:That is because the system wants you to catch exceptions inside of the Task expression, and is warning you that you did not. However, if you do add a try/catch to the expression, then the exception will not be thrown inside of the button_Click() handler, and the only way you could tell that the operation was canceled would be by interrogating the status from the Task, which makes the code flow less well. I am told by someone at Microsoft that they are looking into ways to address this issue, but that it is extremely difficult to address without having a big impact on performance. In the meantime, you can just ignore the exception (either explicitly, or by unchecking the “Break when this exception type is user-unhandled” checkbox.
Summary
The fact that the explanation for what we have done is so much more complex than the code itself gives an idea of how powerful .NET async is. Given that most new processors are based around multiple cores, rather than raw speed, using threads will be the only way to take advantage of the machines’ power. While it has always been possible to hand-roll threading code (something I have done a lot of, sad to say) async lets you encapsulate the work without having to worry too much about the behind-the-scene details.
Just think about what we are doing in this example — we have work going on in another thread, we are not locking up the UI, and we are showing progress to the user and allowing the user to cancel the operation. Not only is the code to do this simpler than it would have been before, but it follows standard patterns that should make it easier for other developers to maintain.
You can download a solution with the example code here.
Hi,
I have a long running method but unfortunately it doesn’t have any loop and it can’t have one. Is there any solution for such scenario. What I have understood from above example is that long running loop must know when to exit the flow on cancellation of passed token.
Thanks
It is hard to answer that generically, but you just generally need to call ct.ThrowIfCancellationRequested(); often enough to make the UI responsive to the cancel request. If you just have a lot of procedural code, you can litter it with calls.
On the other hand, if your code is calling another long-running process, that can be tricky to handle depending on what sort of process it is (ex: if you are making a web request, you can kill your end, but you can’t really kill the actual request).
Thank you for the nice and easy to follow post! I am currently trying to implement a similar pattern only the SlowProcess that I would like to report is located in one class and the UI is in another. Is there a way to pass the method to the UI class for execution? I have asked the question in more detail on stackoverflow here https://stackoverflow.com/questions/51248174/wpf-progressbar-update-from-another-class?noredirect=1#comment89478912_51248174
Thank you!
I’ve spent a bunch of time recently googling this type of stuff. Everyone has a different solution and through your post I finally got it working! Thanks for pointing out the OperationCanceledException!
A brilliant post, much appreciated, very helpful, so thanks for that. You are a great writer but I have some suggestions on how to improve this or future tutorials:
You start out saying, ” I’ll show the full code first, then break it down”. You should always start a tutorial by showing/explaining the concept, the basics, and build on that with more complex or optional details. You do it backwards. Also, you excluded from your full code the essential and important SlowProcess function details that allows ansync magic to happen.
It is of no use to the reader to explain the old way of doing things (ThreadPool.QueueUserWorkItem(SlowProcess);), or to compare and contrast the new with the old, we are here to learn “the magic of async” in a nutshell, as follows:
private async void button_Click(object sender, EventArgs e)
{
statusLabel.Text = “Working”;
await SlowProcess();
statusLabel.Text = “Done”;
}
Please avoid using pronouns like ‘this’ or ‘it’. You wrote. ‘In the example above, SlowProcess() just does some work, but [it] could have returned a value as well:’ Instead of making us go back to find out what ‘it’ is referring to, I would have found the following easier to understand:
await SlowProcess();
could be modified to return a value as follows:
int result = await SlowProcess(prog, m_cancelTokenSource.Token);
Another example of the mis-use of a pronoun that took me a while to understand is as follows:
[This] works in the same manner, but dumping [it] in the middle of the rest of the code makes the code harder to read and manage.
I am/was not sure of ‘this’ and ‘it’ were referring to the example above or below your sentence?
Anyway, hope you take no offense to my suggestions. Thanks for the post.
My SlowProcess function returns a Bitmap object.
I put prog.Report(i); in my SlowProcess function, as suggested, to update the progessbar1
In the calling function I instantiate prog as follows (and can’t remove):
Progress prog = new Progress(UpdateProgressBar);
Problem is, Visual Studio shows an error caused by prog.Report(i);
The error is: argument 1, cannot convert from int to System.Drawing.Bitmap
I don’t understand why/how the SlowProcess return type is interfering with prog.
Please explain if you know,
You mean that the activity returns a Bitmap: private Task SlowProcess(…) ?
If so, then that shouldn’t have any impact on the progress display. I’d need to see the code to figure out what is going wrong.