- Reporting Progress
- Error Handling
- Returning a Result
- Cancellation
A Task is another way to implement a separate worker thread. I will demonstrate how to transform all the actions performed in a background worker into the context of a Task.
I have used the following resources for Tasks
- async-and-await.html
- async-in-4-5-enabling-progress-and-cancellation-in-async-apis/
- http://www.jeremybytes.com/Downloads.aspx#Tasks
- task-and-await-consuming-awaitable.html
Calculate Pi Example
The calculations for Pi are a mystery, but they take time.void CalcPi(int digits) {
StringBuilder pi = new StringBuilder("3", digits + 2);
if( digits > 0 ) {
pi.Append(".");
for( int i = 0; i < digits; i += 9 ) {
int nineDigits = NineDigitsOfPi.StartingAt(i + 1);
int digitCount = Math.Min(digits - i, 9);
string ds = string.Format("{0:D9}", nineDigits);
pi.Append(ds.Substring(0, digitCount));
}
}
A method encapsulates access to the UI.
void ShowProgress(string pi, int totalDigits, int digitsSoFar) {
// Display progress in UI
this.resultsTextBox.Text = pi;
this.calcToolStripProgressBar.Maximum = totalDigits;
this.calcToolStripProgressBar.Value = digitsSoFar;
}
A custom class is used to encapsulate data that is passed to the UI.
class CalcPiUserState {
public readonly string Pi;
public readonly int TotalDigits;
public readonly int DigitsSoFar;
public CalcPiUserState(string pi, int totalDigits, int digitsSoFar) {
this.Pi = pi;
this.TotalDigits = totalDigits;
this.DigitsSoFar = digitsSoFar;
}
}
Helper methods are used to control modify the appearance of the UI.
void SetUIReady()
{
// Reset progress UI
this.calcToolStripStatusLabel.Text = "Ready";
this.calcToolStripProgressBar.Visible = false;
}
void SetUIBusy()
{
// Set calculating UI
this.calcToolStripProgressBar.Visible = true;
this.calcToolStripStatusLabel.Text = "Calculating...";
}
Starting a Worker Thread that Reports Progress
BackgroundWorker
Handle the DoWork eventvoid backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {
CalcPi((int)e.Argument);
}
Set WorkerSupportsProgress property
Handle ReportProgress event
void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) {
// Show progress
CalcPiUserState progress = (CalcPiUserState)e.UserState;
ShowProgress(
progress.Pi, progress.TotalDigits, progress.DigitsSoFar);
}
Call RunWorkerAsync
void calcButton_Click(object sender, EventArgs e) {
SetUIBusy();
// Begin calculating pi asynchronously
this.backgroundWorker.RunWorkerAsync(
(int)this.decimalPlacesNumericUpDown.Value);
}
Handle RunWorkerCompleted event
void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
SetUIReady();
}
Modify the CalcPi method so that it reports progress regularly.
void CalcPi(int digits) {
StringBuilder pi = new StringBuilder("3", digits + 2);
CalcPiUserState state = new CalcPiUserState(pi.ToString(), digits, 0);
// Report initial progress
this.backgroundWorker.ReportProgress(0, state);
if( digits > 0 ) {
pi.Append(".");
for( int i = 0; i < digits; i += 9 ) {
int nineDigits = NineDigitsOfPi.StartingAt(i + 1);
int digitCount = Math.Min(digits - i, 9);
string ds = string.Format("{0:D9}", nineDigits);
pi.Append(ds.Substring(0, digitCount));
// Report continuing progress
state = new CalcPiUserState(pi.ToString(), digits, i + digitCount);
this.backgroundWorker.ReportProgress(0, state);
// Check for cancelation
if (this.backgroundWorker.CancellationPending) { return; }
}
}
}
Task
Create a helper method to update the UI from the custom state class.void ShowProgressState(CalcPiUserState state)
{
try
{
// Display progress in UI
this.resultsTextBox.Text = state.Pi;
this.calcToolStripProgressBar.Maximum = state.TotalDigits;
this.calcToolStripProgressBar.Value = state.DigitsSoFar;
}
catch (Exception e)
{
Console.WriteLine("{0} in ShowProgressState", e.Message);
throw e;
}
}
Create a method with return type Task that encapsulates the work. In order to report progress, create an Progress delegate with an Action that reports progress. CalcPiWithProgress is CPU-bound, so call it with Task.Run. The async keyword in the signature means that await is a keyword in the body of the method.
async Task DoWork()
{
//Any method that is an Action, has one parameter of the gerenic type and returns void.
Action
//The Action and the Progress must have the same generic type.
IProgress
await Task.Run(() => CalcPiWithProgress(
(int)this.decimalPlacesNumericUpDown.Value, progress)
);
}
Start task. The await keyword causes the method to start asynchronously. When the thread completes, the code after the await will be run in the same thread context. The handler does not block: SetUIReady is called, but only after the thread completes, and well after the handler completes.
async void calcButton_Click(object sender, EventArgs e)
{
SetUIBusy();
// Begin calculating pi asynchronously
await DoWork();
SetUIReady();
}
A special method is not needed to catch the completion of the thread, as with a background worker.
Modify the CalcPi method so it reports progress.
void CalcPiWithProgress(int digits, IProgress
StringBuilder pi = new StringBuilder("3", digits + 2);
CalcPiUserState state = new CalcPiUserState(pi.ToString(), digits, 0);
// Report initial progress
if (progress != null)
progress.Report(state);
if( digits > 0 ) {
pi.Append(".");
for( int i = 0; i < digits; i += 9 ) {
int nineDigits = NineDigitsOfPi.StartingAt(i + 1);
int digitCount = Math.Min(digits - i, 9);
string ds = string.Format("{0:D9}", nineDigits);
pi.Append(ds.Substring(0, digitCount));
// Report continuing progress
state = new CalcPiUserState(pi.ToString(), digits, i + digitCount);
if (progress != null)
progress.Report(state);
}
}
}
Catch Exceptions
Background Worker
Test the Error property in the RunWorkerCompletedEventArgs in the RunWorkerCompleted handler.void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
SetUIReady();
// Was there an error?
if( e.Error != null ) {
this.resultsTextBox.Text = e.Error.Message;
return;
}
}
Task
With Task, the code after the await is run on the UI thread. The code after the await is executed after the thread completes. The handler would have completed long ago.Catch the exception and reset the UI.
async void calcButton_Click(object sender, EventArgs e)
{
try
{
SetUIBusy();
// Begin calculating pi asynchronously
await DoWork();
}
catch (Exception ex)
{
this.resultsTextBox.Text = ex.Message;
}
finally
{
SetUIReady();
}
}
Return a Result from the Thread
Background Worker
Set the result of the thread in the DoWorkEventArgs in the DoWork handler.void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {
// Track start time
DateTime start = DateTime.Now;
CalcPi((int)e.Argument);
// Return elapsed time
DateTime end = DateTime.Now;
TimeSpan elapsed = end - start;
e.Result = elapsed;
}
Retrieve the result from the RunWorkerCompletedEventArgs in the RunWorkerCompleted handler.
void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
SetUIReady();
// e.Result will cause an error if the thread threw an exception
if( e.Error != null ) {
this.resultsTextBox.Text = e.Error.Message;
return;
}
// Show elapsed time
TimeSpan elapsed = (TimeSpan) e.Result;
MessageBox.Show("Elapsed: " + elapsed.ToString());
}
Task
Instead of using Task, use Taskasync Task
{
// Track start time
DateTime start = DateTime.Now;
//Any method that is an Action, has one parameter of the gerenic type and returns void.
Action
//The Action and the Progress must have the same generic type.
IProgress
await Task.Run(() => CalcPiWithProgress(
(int)this.decimalPlacesNumericUpDown.Value, progress)
);
// Return elapsed time
DateTime end = DateTime.Now;
TimeSpan elapsed = end - start;
return elapsed;
}
The result will be assigned from the await command.
async void calcButton_Click(object sender, EventArgs e)
{
try
{
SetUIBusy();
// Begin calculating pi asynchronously
TimeSpan elapsed = await DoWork();
// Show elapsed time
MessageBox.Show("Elapsed: " + elapsed.ToString());
}
catch (Exception ex)
{
this.resultsTextBox.Text = ex.Message;
}
finally
{
SetUIReady();
}
}
Adding the Ability to Cancel
Background Worker
Set the WorkerSupportsCancellation property
Signal that the thread should stop.
void calcButton_Click(object sender, EventArgs e) {
// Don't process if cancel request pending
// (Should not be called, since we disabled the button...)
if( this.backgroundWorker.CancellationPending ) { return; }
// If worker thread currently executing, cancel it
if( this.backgroundWorker.IsBusy ) {
this.calcButton.Enabled = false;
this.backgroundWorker.CancelAsync();
return;
}
SetUIBusy();
// Begin calculating pi asynchronously
this.backgroundWorker.RunWorkerAsync(
(int)this.decimalPlacesNumericUpDown.Value);
}
The thread monitors the flag for cancellation. When requested, the thread stops working.
void CalcPi(int digits)
{
StringBuilder pi = new StringBuilder("3", digits + 2);
CalcPiUserState state = new CalcPiUserState(pi.ToString(), digits, 0);
// Report initial progress
this.backgroundWorker.ReportProgress(0, state);
// Check for cancelation
if (this.backgroundWorker.CancellationPending) { return; }
if( digits > 0 ) {
pi.Append(".");
for( int i = 0; i < digits; i += 9 ) {
int nineDigits = NineDigitsOfPi.StartingAt(i + 1);
int digitCount = Math.Min(digits - i, 9);
string ds = string.Format("{0:D9}", nineDigits);
pi.Append(ds.Substring(0, digitCount));
// Report continuing progress
state = new CalcPiUserState(pi.ToString(), digits, i + digitCount);
this.backgroundWorker.ReportProgress(0, state);
// Check for cancelation
if (this.backgroundWorker.CancellationPending) { return; }
}
}
}
In the RunWorkerCompleted handler check the Cancelled property in the event args.
void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
// Reset progress UI
this.calcButton.Text = "Calculate";
this.calcButton.Enabled = true;
this.calcToolStripStatusLabel.Text = "Ready";
this.calcToolStripProgressBar.Visible = false;
// Was the worker thread cancelled?
if (e.Cancelled)
{
this.resultsTextBox.Text = "Cancelled";
}
else
{
// Show elapsed time
TimeSpan elapsed = (TimeSpan)e.Result;
MessageBox.Show("Elapsed: " + elapsed.ToString());
}
Task
As the thread is asked to respond in more ways, it is useful separate the actions performed when the thread ends. As part of the Task class, use the Continue with to specify a method that has the format for Action. Action
async void calcButton_Click(object sender, EventArgs e)
{
SetUIBusy();
// Begin calculating pi asynchronously
await DoWork().ContinueWith(WorkCompleted,
TaskScheduler.FromCurrentSynchronizationContext());
}
Many states of the task can be inspected. This version does the same as the earlier version of the application. Next, cancellation will be added.
private void WorkCompleted(Task
{
switch (task.Status)
{
case TaskStatus.Canceled:
break;
case TaskStatus.Faulted:
this.resultsTextBox.Text = task.Exception.ToString();
break;
case TaskStatus.RanToCompletion:
MessageBox.Show("Elapsed: " + task.Result.ToString());
break;
default:
break;
}
SetUIReady();
}
For cancellation, CancellationTaskSource is used. It contains a token that can be set. The token can be monitored by the thread.
CancellationTokenSource tokenSource;
CancellationToken token;
public MainForm() {
InitializeComponent();
tokenSource = new CancellationTokenSource();
token = tokenSource.Token;
SetUIReady();
}
A new helper will be added to report progress and check for cancellation, with the ThrowIfCancellationRequested() method of the token.
void ProgressTestCancel(
IProgress
{
// Report progress
if (progress != null)
progress.Report(state);
token.ThrowIfCancellationRequested();
}
The worker thread reports progress and tests for cancellation at regular intervals.
void CalcPiWithProgress(
int digits, IProgress
{
StringBuilder pi = new StringBuilder("3", digits + 2);
CalcPiUserState state = new CalcPiUserState(pi.ToString(), digits, 0);
// Report initial progress
ProgessTestCancel(progess, token, state);
if( digits > 0 ) {
pi.Append(".");
for( int i = 0; i < digits; i += 9 ) {
int nineDigits = NineDigitsOfPi.StartingAt(i + 1);
int digitCount = Math.Min(digits - i, 9);
string ds = string.Format("{0:D9}", nineDigits);
pi.Append(ds.Substring(0, digitCount));
// Report continuing progress
state = new CalcPiUserState(pi.ToString(), digits, i + digitCount);
ProgessTestCancel(progess, token, state);
}
}
}
Pass the token into the method that does the asynchronous work. The task will be needed in other parts of the application to improve the interaction of the UI, so the task is added as an instance variable. Does this remind you of background worker???
//Needed in order to test if Task is busy in other methods
Task
async Task
{
// Track start time
DateTime start = DateTime.Now;
//Any method that is an Action, has one parameter of the gerenic type and returns void.
Action
//The Action and the Progress must have the same generic type.
IProgress
piTask = new Task
TimePiThread(
(int)this.decimalPlacesNumericUpDown.Value, progress, tokenSource.Token)
, tokenSource.Token);
piTask.Start();
return await piTask;
}
The button handler will have two possible states: ready to run, ready to cancel. Update the UI based on the state of the task. If the task is running, cancel it on click.
async void calcButton_Click(object sender, EventArgs e)
{
// Don't process if cancel request pending
// Should not happen, since button will be disabled
if (this.tokenSource.IsCancellationRequested) { return; }
if (piTask != null && !piTask.IsCompleted)
{
this.calcButton.Enabled = false;
tokenSource.Cancel();
}
else
{
SetUIBusy();
// Begin calculating pi asynchronously
await DoWork().ContinueWith(WorkCompleted,
TaskScheduler.FromCurrentSynchronizationContext());
}
}
The helpers for updating the UI need modification, too:
void SetUIReady()
{
// Reset progress UI
this.calcToolStripStatusLabel.Text = "Ready";
this.calcToolStripProgressBar.Visible = false;
this.calcButton.Text = "Calculate";
this.calcButton.Enabled = true;
}
void SetUIBusy()
{
// Set calculating UI
this.calcToolStripProgressBar.Visible = true;
this.calcButton.Text = "Cancel";
this.calcToolStripStatusLabel.Text = "Calculating...";
}
No comments:
Post a Comment