0% found this document useful (0 votes)
10 views

Handling Special Scenarios - Parallel Programming With C# and .NET - Fundamentals of Concurrency and Asynchrony Behind Fast-Paced Applications

Uploaded by

YeePee Indo
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
10 views

Handling Special Scenarios - Parallel Programming With C# and .NET - Fundamentals of Concurrency and Asynchrony Behind Fast-Paced Applications

Uploaded by

YeePee Indo
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 39

2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .

NET: Fundamentals of Concurrency and Asynchrony B…

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2024
V. Sarcar, Parallel Programming with C# and .NET
https://doi.org/10.1007/979-8-8688-0488-5_2

2. Handling Special Scenarios


Vaskaran Sarcar1
(1) Near Garia Station, Post: Garia, Kuntala Furniture, 2nd Floor, Kolkata, West
Bengal, India
In Chapter 1, you learned about various aspects of tasks including contin-
uation tasks and nested tasks. In this chapter, I’ll continue the discussion
of task programming, but this time the focus will be handling special sce-
narios such as exceptions and cancellations. Let’s get started.

Introduction to Exceptions

In a multithreaded environment, handling exceptions can be tricky. The


reason is obvious: different tasks may throw different exceptions. Since
you are reading about the advanced concepts of programming, I assume
you are familiar with the fundamentals of exceptions and how to handle
them in a C# project, so I will not discuss the basics in this book. Instead,
I’ll focus on possible exceptional scenarios when you program with tasks
in a multithreaded environment.

Understanding the Challenge

Let’s start with a simple class, called Product. This class has a method,
called CheckUser, to identify whether a user is a valid user. For simplicity,
let’s assume the following characteristic: Each valid user ID starts with
u. Otherwise, the CheckUser method throws an instance of
UnauthorizedAccessException.

Demonstration 1

Now let me hack the system to create this exception and handle it inside a try-
catch block. Here is a sample demonstration:

using static System.Console;


WriteLine("Exception handling demo.");
try
{
var validateUser = Product.CheckUser("abc");
WriteLine(validateUser);
}
catch (Exception e)
{
WriteLine($"Caught error: {e.Message}");
}
class Product
{
/// <summary>
/// This method throws an exception when the user ID does
/// not start with 'u'
/// </summary>
/// <param name="userId"> The user ID</param>

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 1/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
/// <returns>Confirming the valid user</returns>
/// <exception cref="UnauthorizedAccessException"> The
/// exception is thrown when the <paramref
/// name="userId" is an invalid ID </exception>
public static string CheckUser(string userId)
{
string msg;
if (userId.StartsWith("u"))
{
msg = $"{userId} is a valid user";
}
else
{
throw new UnauthorizedAccessException($"Id:
{userId} is invalid.");
}
return msg;
}
}

Output

There is no surprise that upon executing this program, you’ll see the following
output:

Exception handling demo.


Caught error: Id: abc is invalid.

You are probably familiar with this type of program and there is nothing
new up to this point.

POINT TO NOTE
Visual Studio can still display this exception if you want. For example, in Visual
Studio’s Exception Settings (Ctrl+Alt+E), if you want your program to break when
a particular exception is thrown, you can enable the checkbox for that particular
exception. For example, I have the settings shown in Figure 2-1 for the common
language runtime exceptions.

Figure 2-1 Exception Settings for Common Language Runtime exceptions in Visual Studio

While executing the program, you will see the exception shown in Figure 2-2.

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 2/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

Figure 2-2 Visual Studio displays the UnauthorizedAccessException while the program was
running

Note that the program has not crashed yet; you can click the Continue
button to resume the execution.

Demonstration 2

Now change the program a little bit and analyze the situation again. This time,
let’s invoke the CheckUser method through a task that is created inside the main
thread, as shown in the following program:

using static System.Console;


WriteLine("Exception handling demo.");
try
{
var validateUser = Task.Run(() => Product.CheckUser("abc"));
WriteLine("End");
}
catch (Exception e)
{
WriteLine($"Caught error: {e.Message}");
}
// The Product class is the same. It is not shown to avoid
// repetition.

Output

Upon executing this program, you may see the following output:

Exception handling demo.


End

You can see that this time the output does not show anything about the
exception. Why does this happen? Notice that this time the main thread
did not encounter the exception; it was encountered by the task called
validateUser that was created by this main thread. However, an unob-
served exception can cause problems at a later stage. So, you may be in-
terested in watching all the exceptions and decide to handle them as per
the priority. This is an important note for you to remember.

POINT TO NOTE
I’d like to remind you that many developers like to use Task.Run instead
of Task.Factory.StartNew. I also belong to that category. So, from this
time onward, I’ll be using Task.Run much more compared to
Task.Factory.StartNew. In this context, you may want to revisit Q&A
1.2 in Chapter 1. In the end, it’s up to you, so choose the approach that
you prefer. Once you finish Chapter 6 of this book, you can read an on-

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 3/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

line post comparing these approaches at


https://devblogs.microsoft.com/pfxteam/task-run-vs-task-fac-
tory-startnew/. Though the article was written a while ago, but it is still
useful.

Retrieving the Error Details

How can I show you information about the exception? An obvious way is to
handle the exception inside the task itself. For example, I can replace the
following line:

var validateUser = Task.Run(() => Product.CheckUser("abc"));

with something like the following:

var validateUser = Task.Run(


() =>
{
string msg = string.Empty;
try { msg = Product.CheckUser("abc"); }
catch (Exception e)
{
WriteLine($"Caught error inside the task:
{e.Message}");
}
return msg;
});

However, you understand that inside the main thread, we can launch
many tasks, and if we code like this, there will be a lot of code
duplication.

So, let’s search for alternative approaches. Interestingly, in the main


thread, if you use WriteLine(validateUser.Result); or
validateUser.Wait(); inside a try block, you can observe the excep-
tions. Let’s see the next program.

Demonstration 3

Here is a sample demonstration where I use the statement


validateUser.Wait(); inside the try block as follows:

// Previous code as it is
try
{
//Product.CheckUser("abc");
var validateUser = Task.Run(() => Product.CheckUser("abc"));
validateUser.Wait();
WriteLine("End");
}
// There is no change in the remaining code as well

Output

Once you execute the program again, you will see the following output:

Exception handling demo.

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 4/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
Caught error: One or more errors occurred. ('abc' is an invalid user.)

The output depicts that abc is an invalid user and it was the key information
that I wanted to display. If you put a breakpoint inside the catch block as shown
in Figure 2-3 and debug this code, you can see the values shown.

Figure 2-3 A partial snapshot from Visual Studio that shows an InnnerException

You can see that “'abc' is an invalid user.” is an InnerException.


For a situation like this, we can simply pass through the inner exceptions
and display the error detail.

Q&A Session

Q2.1 In the previous output I can see that “'abc' is an invalid


user.” was wrapped as an InnerException. Is there any specific rea-
son for this?

Good question. It is because the program caught the AggregateException


exception. To see this, you can slightly modify the catch block as follows:

catch (Exception e)
{
WriteLine($"Caught error: {e.Message}");
WriteLine($"Exception name: {e.GetType().Name}");
}

If you execute the application again, you will see the following output:

Exception handling demo.


Caught error: One or more errors occurred. ('abc' is an invalid user.)
Exception name: AggregateException

Now the question is, what is an AggregateException? Do not worry! You


will get familiar with it in the following section.

Consider Using AggregateException

When you consider exception handling, we try to identify the potential


error locations and place the try-catch (or try-catch-finally) block accord-
ingly. There should be no confusion about this. However, in the case of
task programming, exception handling is a challenging task. This is be-
cause, during the running state of an application, different tasks can
throw different exceptions at various levels. You have also seen that if

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 5/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

you are not careful enough, some of the exceptions can go unnoticed as
well. And this is an obvious problem because any exception can cause a
system to move into an inconsistent state. So, the question is, how can we
handle exceptions in task programming?

There are indeed different approaches, but the first thing that you should
note is that in the System namespace, there is a class called
AggregateException that inherits from the Exception class. Instead of using
any other exception class, you should prefer this class. This class is specially
designed to handle the exception handling scenarios when you deal with tasks.
Microsoft states the following (see https://learn.microsoft.com/en-
us/dotnet/standard/parallel-programming/exception-handling-
task-parallel-library):
To propagate all the exceptions back to the calling thread, the
Task infrastructure wraps them in an AggregateException in-
stance. The AggregateException exception has an
InnerExceptions property that can be enumerated to examine all
the original exceptions that were thrown, and handle (or not handle)
each one individually.

This is why you will see that AggregateException is used heavily in the
development code to consolidate multiple failures/errors in concurrent
environments, particularly, when we use Task Parallel Library (TPL)
and/or Parallel LINQ(PLINQ). This is why next time onward you will see
me using AggregateException inside the catch block as well.

NotePLINQ is discussed in Chapter 5.

Exception Management

How should we handle exceptions? Different programming model follows


different strategies. For example, if you follow object-oriented programming
(OOP), you’d like to use try, catch, and finally blocks. However, these are
typically absent in functional programming (FP). In my other book Introducing
Functional Programming Using C#, I had a detailed discussion on this topic. For
now, we do not need to investigate those details. Instead, let’s simplify the overall
strategies by putting them into the following categories:

Handling possible exceptions in a single location


Handling possible exceptions in multiple locations

Handling Exceptions in a Single Location

In this section let’s consider the first category, i.e., how to handle the pos-
sible exceptions in one place.

Demonstration 4

Here is the complete program where I simulate the exceptional situations using
some simple code. In this program, in addition to the Product class, you will see
another class called Database. This class contains a method called StoreData
that can throw an InsufficientMemoryException when a user tries to store
more than 500 MB data. Here is the sample code for this:

class Database
{
/// <summary>
/// This method throws an exception when
/// requested size is greater than 500MB
/// </summary>

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 6/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
/// <param name="sizeInMB"> The requested size for
/// allocation in MB</param>
/// <returns> Confirming the storage</returns>
/// <exception cref="InsufficientMemoryException"> The
/// exception is thrown when the <paramref
/// name="sizeInMB" is greater than 500 </exception>
public static string StoreData(int sizeInMB)
{
string allocation;
if (sizeInMB > 500)
{
throw new InsufficientMemoryException($"Cannot
store {sizeInMB} MB data.");
}
else
{
// Some code for allocation
allocation = $"{sizeInMB} is allocated";
}
return allocation;
}
}

Now I create two different tasks inside the main thread and simulate the code
in a way that both tasks encounter exceptions. As discussed in the previous
section, it will be sufficient for you to pass through the inner exceptions and
display the error details. Go through the complete program now:

using static System.Console;


WriteLine("Exception handling demo.");
try
{
var task1 = Task.Run(() => Product.CheckUser("abc"));
var task2 = Task.Run(() => Database.StoreData(501));
Task.WaitAll(task1, task2);
}
catch (AggregateException ae)
{
foreach (Exception e in ae.InnerExceptions)
{
WriteLine($"Caught error: {e.Message}");
}
}
// The Product class and the Database classes are not shown
// to avoid repetition.

Output

Here is some sample output from this program:

Exception handling demo.


Caught error: Id: abc is invalid.
Caught error: Cannot store 501 MB data.

Alternative Approaches

You have seen a simple approach to handling exceptions. Now let me


show you two more approaches that can be used in a similar context.

Alternative Approach 1

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 7/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
Notice the previous catch block. You can see that I used ae.InnerExceptions
to display the errors in the output. Here is an alternative version where I flatten
the inner instances and then start traversing the exceptions as follows:

catch (AggregateException ae)


{
// Alternative approach-1
var exceptions = ae.Flatten().InnerExceptions;
foreach (Exception e in exceptions)
{
WriteLine($"Caught error: {e.Message}");
}
}

Alternative Approach 2

In the AggregateException class, you can see a method called Handle that
has the following form:

public void Handle(Func<Exception, bool> predicate)


{
// The method body is not shown
}

Using this method, you can invoke a handler on each exception contained in
an AggregateException. For example, let’s replace the foreach loop inside
the catch block in Demonstration 4 with a new block of code as follows (I have
shown all the different approaches that we have discussed so far using code
comments for easy comparison):

catch (AggregateException ae)


{
//// Initial approach
// foreach (Exception e in ae.InnerExceptions)
// {
// WriteLine($"Caught error: {e.Message}");
// }
//// Alternative approach-1
// var exceptions = ae.Flatten().InnerExceptions;
// foreach (Exception e in exceptions)
// {
// WriteLine($"Caught error: {e.Message}");
//}
// Alternative approach-2
ae.Handle(e =>
{
WriteLine($"Caught error: {e.Message}");
return true;
});
}

If you execute the program now, you will see the same output.

Q&A Session

Q2.2 I can see that you have thrown two different exceptions from
two different tasks. If both tasks throw the same type of exception,
how can I distinguish them?

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 8/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
This is easy. You can associate the task IDs with the exception’s Source property.
For example, if you replace the following lines:

throw new UnauthorizedAccessException($"Id: {userId} is


invalid.");
throw new InsufficientMemoryException($"Cannot store {sizeInMB} MB data.");

with the following lines in the previous program:

throw new UnauthorizedAccessException($"Id: {userId} is


invalid."){ Source = Task.CurrentId.ToString() };
throw new InsufficientMemoryException($"Cannot store {sizeInMB}
MB data.") { Source = Task.CurrentId.ToString() };

and modify the catch block as follows:

catch (AggregateException ae)


{
ae.Handle(e =>
{
WriteLine($"Caught error: {e.Message}
Source: {e.Source}");
return true;
});
}

then you can get output like the following:

Exception handling demo.


Caught error: Id: abc is invalid. Source: 1
Caught error: Cannot store 501 MB data. Source: 2

Similarly, if multiple tasks throw the same exception, you can set the
Source property to identify the source of the error.

Demonstration 5

Here is a complete program to illustrate the idea:

WriteLine("Q&A on exception handling.");


try
{
var task1 = Task.Run(
() => throw new InsufficientMemoryException($"Cannot
store 500 MB data."){ Source = "task1" });
var task2 = Task.Run(
() => throw new InsufficientMemoryException($"Cannot
store 500 MB data."){ Source = "task2" });
Task.WaitAll(task1, task2);
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
WriteLine($"Caught error: {e.Message} [From
{e.Source}]");
return true;
});
}

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 9/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
Output

Here is the sample output:

Q&A on exception handling.


Caught error: Cannot store 500 MB data. [From task1]
Caught error: Cannot store 500 MB data. [From task2]

Handling Exceptions in Multiple Locations

I assume that you have gotten the idea of how to handle multiple excep-
tions. This is OK and probably the most common approach. Next, I will
show you a mechanism where you can handle one part of the aggregate
exception in one place and the remaining part in another place. More
specifically, you propagate this remaining part of the exception up to the
hierarchy and handle it there. There is a reason for this: I want you to
show the effectiveness of the Handle method.

First, see the following code. This code fragment indicates that you catch the
probable set of exceptions but handle only one of them:
InsufficientMemoryException.

// Some code before


catch (AggregateException ae)
{
ae.Handle(
e =>
{
if (e is InsufficientMemoryException)
{
WriteLine($"Caught error: {e.Message}");
return true;
}
else
{
return false;
}
});
}

By returning true, you indicate that this particular exception is handled.


More specifically, this code fragment says that you like to handle only the
InsufficientMemoryException but no other exceptions in this location.

Demonstration 6

For example, in the upcoming program, the main thread calls the InvokeTasks
method that, in turn, creates and runs three new tasks as follows:

var task1 = Task.Run(() => Product.CheckUser("abc"));


var task2 = Task.Run(() => Database.StoreData(501));
var task3 = Task.Run(
() => throw new DllNotFoundException("the dll is missing!"));

The first two tasks are already shown in the previous demonstration. You
know that these tasks will raise exceptions. The third task, named task3, is
added for the sake of discussion so that you do not assume that you need to
handle an equal number of tasks in each location.

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 10/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

You’ll see that I catch all the possible sets of exceptions inside
InvokeTasks but handle only one of them:
InsufficientMemoryException. As a result, the remaining exceptions
will be passed up to the calling hierarchy. So, I have handled them inside
the Main method aka main thread. Let’s go through the complete program
now.

NoteI remind you that I have heavily used top-level statements and en-
abled implicit using statements for the C# projects in this book. These
features came in C# 9.0 and C# 10.0, respectively. This is why you do not
see a method named Main in this program.

using static System.Console;


WriteLine("Exception handling demo.");
try
{
InvokeTasks();
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
WriteLine($"Caught error inside Main(): {e.Message}");
return true;
});
}
static void InvokeTasks()
{
try
{
var task1 = Task.Run(() => Product.CheckUser("abc"));
var task2 = Task.Run(() => Database.StoreData(501));
var task3 = Task.Run(() => throw new
DllNotFoundException("the dll is missing!"));
Task.WaitAll(task1, task2, task3);
}
catch (AggregateException ae)
{
// Handling only InsufficientMemoryException, others
// will be propagated up to the hierarchy
ae.Handle(
e =>
{
if (e is InsufficientMemoryException)
{
WriteLine($"Caught error inside
InvokeTasks(): {e.Message}");
return true;
}
else
{
return false;
}
});
}
}
// There was no change inside the "Product" class and the
// "Database" class. So, they are not shown again to avoid
// repetitions.

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 11/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

NoteYou can download the project


Demo6_HandlingExceptionsSeparately from the Apress website to see
and execute the complete program.

Output

Here is some sample output from this program:

Exception handling demo.


Caught error inside InvokeTasks(): Cannot store 501 MB data.
Caught error inside Main(): Id: abc is invalid.
Caught error inside Main(): the dll is missing!

Final Suggestion

I hope you have gotten an idea of how to handle exceptions in a parallel


execution environment. I’d like to share one final thought with you: if you
do not handle exceptions within tasks, you should try to handle them as
close as possible to those places where you wait for the task completion
and/or retrieve the result of the task invocation.

Understanding Cancellations

If you want to open a fixed deposit (FD) in India, banks give you two op-
tions: callable FD and non-callable FD. Usually, the interest rates for non-
callable FDs are slightly higher than their counterpart. This is because
you are not allowed to cancel the FD and withdraw the amount before the
specified date. But you know that in a crisis, a customer may need to can-
cel the FD. So, if the customer does not have sufficient backup, he may opt
for a callable FD. Similar situations are observed in programming as well.
You may need to work with several tasks, and while those tasks are run-
ning, you may need to cancel one or more of them due to various reasons.
Some of the examples include terminating a long-running process or re-
leasing a critical resource. This is why you may like to work with can-
cellable tasks. Let’s see how to cancel a task.

Different Ways of Cancellation

To support possible cancellations, this time onward you’ll see me using


cancellation tokens. To generate such a token, you need the instances of the
CancellationTokenSource and CancellationToken. You may see the
following lines of code in this context:

CancellationTokenSource tokenSource = new();


CancellationToken token = tokenSource.Token;

Obviously, using the var keyword, you can see a slight variation as follows:

var tokenSource = new CancellationTokenSource();


var token = tokenSource.Token;

Next, you pass this token to the intended task. Earlier, you saw (in Figure 1-
1 in Chapter 1) that the Task constructor has several overloaded versions. Some
of them accept a CancellationToken instance as a method parameter. Here is
an example:

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 12/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

public Task(Action action, CancellationToken cancellationToken);

The StartNew method of the TaskFactory class has the overloaded


versions as well, and some of them also accept a CancellationToken instance
as a method parameter. Here is an example:

public Task StartNew(Action action, CancellationToken


cancellationToken);

Similarly, the Run method of the Task class has similar overloads.

These constructs give you a clue on how to pass a CancellationToken


instance to a task. Shortly, you’ll see me using the following version:

public static Task Run(Action action, CancellationToken


cancellationToken)

In the upcoming demonstration, I created a task that can keep printing the
numbers from 0 to 99. Nowadays computer processors are very fast. So, this task
can finish its execution very fast. To prevent this, I impose a short sleep after it
prints a number. I also opt for the support for cancellation while it executes. So, I
create a CancellationToken instance, called token, and pass it as follows:

var printTask=Task.Run
(
() =>
{
// A loop that runs 100 times
for (int i = 0; i < 100 ; i++)
{
// Some other code is skipped here
WriteLine($"{i}");
// Imposing the sleep to make some delay
Thread.Sleep(500);
}
}, token
);

However, you must remember the following guidelines from Microsoft (see
https://learn.microsoft.com/en-us/dotnet/standard/parallel-
programming/how-to-cancel-a-task-and-its-children):
The calling thread does not forcibly end the task; it only signals
that cancellation is requested. If the task is already running, it is up
to the user delegate to notice the request and respond appropriately.

NoteThis prior message indicates that it is possible that by the time a call-
ing thread raises a cancellation request, the running task finishes its exe-
cution. So, if you want to cancel a running task, you should raise the can-
cellation request as soon as possible.
Let me show you some cancellation techniques.

Initial Approach

In the first approach, you can use an if condition to evaluate whether the
cancellation request is raised. If so, you can do something before you can-
cel the task. For example, you can introduce a message saying that this
task is going to cancel itself (and clean up the resources, if required), and
then you can put a break or return statement. Probably, most of us are

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 13/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

aware of this kind of “soft exit” mechanism. Let me show you an


example.

Demonstration 7

The following demonstration shows you a sample where I added a new code
block in the previous code segment and made it bold for your reference:

using static System.Console;


WriteLine("Simple cancellation demonstration.");
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var printTask = Task.Run
(
() =>
{
// A loop that runs 100 times
for (int i = 0; i < 100; i++)
{
if (token.IsCancellationRequested)
{
WriteLine("Cancelling the print activity.");
// Do some cleanups, if required
return;
}
WriteLine($"{i}");
// Imposing the sleep to make some delay
Thread.Sleep(500);
}
}, token
);
WriteLine("Enter c to cancel the task.");
char ch = ReadKey().KeyChar;
if (ch.Equals('c'))
{
WriteLine("\nTask cancellation requested.");
tokenSource.Cancel();
}
// Wait till the task finishes the execution
while (!printTask.IsCompleted) { }
WriteLine($"The final status of printTask is: {printTask.Status}");
WriteLine("End of the main thread.");

POINT TO NOTE
This program uses the following line:

while (!someTask.IsCompleted) { }

Microsoft suggests you avoid this kind of polling in the production code
as it is very inefficient (see https://learn.microsoft.com/en-
us/dotnet/standard/parallel-programming/exception-handling-
task-parallel-library). So, a better solution can be made with
printTask.Wait();. But if I use the Wait() method, I need to guard the
possible exceptions using try-catch blocks. To avoid this, I have used
this line of code in this program and the next few demonstrations be-
cause I wanted to focus purely on cancellations, instead of mixing it up
with exceptions.

Output

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 14/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
Here is some sample output:

Simple cancellation demonstration.


Enter c to cancel the task.
0
1
2
c
Task cancellation requested.
Cancelling the print activity.
The final status of printTask is: RanToCompletion
End of the main thread.

Q&A Session

Q2.3 In the previous demonstration, you canceled the task, but in the
output, the final status of the task was displayed as RanToCompletion,
instead of Canceled. Is this a bug?

No. Let’s see what Microsoft says about it. The online documentation states
the following (see https://learn.microsoft.com/en-
us/dotnet/standard/parallel-programming/task-cancellation):
In the Task classes, cancellation involves cooperation between the
user delegate, which represents a cancelable operation, and the code
that requested the cancellation. A successful cancellation involves
the requesting code calling the CancellationTokenSource.Cancel
method and the user delegate terminating the operation in a timely
manner. You can terminate the operation by using one of these op-
tions:

By returning from the delegate. In many scenarios, this option is


sufficient. However, a task instance that's canceled in this way transi-
tions to the TaskStatus.RanToCompletion state, not to the
TaskStatus.Canceled state.
By throwing an OperationCanceledException and passing it the
token on which cancellation was requested. The preferred way to per-
form this is to use the ThrowIfCancellationRequested method. A
task that's canceled in this way transitions to the Canceled state,
which the calling code can use to verify that the task responded to its
cancellation request.

The first bullet point is easy to understand and justifies the final status
of printTask in Demonstration 7. In the next section, I’ll show you the
other approach where you will notice that the final status is Canceled.

Alternative Approach

Let’s see the alternative ways of cancellations as well. In a similar context,


developers often like to throw an OperationCanceledException exception as
follows:

if (token.IsCancellationRequested)
{
WriteLine("Cancelling the print activity.");
throw new OperationCanceledException(token);
}

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 15/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
Demonstration 8

It is time for another demonstration. You can update the task definition in
Demonstration 7 as follows:

var printTask = Task.Run


(
() =>
{
// A loop that runs 100 times
for (int i = 0; i < 100; i++)
{
// Approach-1
// if (token.IsCancellationRequested)
// {
// WriteLine("Cancelling the print activity.");
// // Do some cleanups, if required
// return;
// }
// Approach-2
if (token.IsCancellationRequested)
{
WriteLine("Cancelling the print activity.");
// Do some cleanups, if required
throw new OperationCanceledException(token);
}
WriteLine($"{i}");
// Imposing the sleep to make some delay
Thread.Sleep(500);
}
}, token
);

Now execute the program again.

Output

Here is a sample output. Notice that in Demonstration 7, the final status of the
task was RanToCompletion, but in this demonstration, it appears as Canceled.

Simple cancellation demonstration.


Enter c to cancel the task.
0
1
2
c
Task cancellation requested.
Cancelling the print activity.
The final status of printTask is: Canceled
End of the main thread.

Shortening the Code

Microsoft says that the ThrowIfCancellationRequested method is the


functional equivalent of the following lines (see
https://learn.microsoft.com/en-
us/dotnet/api/system.threading.cancellationtoken.throwifcancellationrequested?
view=net-8.0):

if (token.IsCancellationRequested)
throw new OperationCanceledException(token);

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 16/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
This implies that you can use the ThrowIfCancellationRequested
method for the following two things:

You can check whether a cancellation request is raised.


You can throw the OperationCanceledException exception when
such a request is raised.

This is why you can shorten the code as follows:

var printTask = Task.Run


(
() =>
{
// A loop that runs 100 times
for (int i = 0; i < 100; i++)
{
// Approach-3
token.ThrowIfCancellationRequested();
WriteLine($"{i}");
// Imposing the sleep to make some delay
Thread.Sleep(500);
}
}, token
);

This is a common and widely used approach. Normally, I prefer to use this
approach. If you like to do some cleanups before this call, you can write
something like the following as well:

if (token.IsCancellationRequested)
{
// Do some cleanups, if required
token.ThrowIfCancellationRequested();
}

Q&A Session

Q2.4 In Demonstration 7, you simply did a soft exit and got the final
task status as RanToCompletion, whereas in Demonstration 8, the fi-
nal task status was canceled. I understand that this is a design deci-
sion, but I’d like to know your thoughts on them.

Normally, I’d like to follow the approach that is shown in Demonstration


8. This is because in an enterprise application, we normally deal with sev-
eral tasks, and we often need to understand the log/output. In those cases,
I can go through the log to understand which task was canceled. But if
you simply exit from the method without doing anything, there is no such
record left for you.

One Final Note

At the time of this writing, the OperationCanceledException class has 7


overloaded constructors that may take 0, 1 or 2 parameters. You can use them to
initialize a new instance of OperationCanceledException as per your needs.
Here I include some of them for your reference:

OperationCanceledException(CancellationToken): Initializes a
new instance with a cancellation token. (You have already seen this in
Demonstration 8).

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 17/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

OperationCanceledException(String): Initializes a new instance


with a specified error message.
OperationCanceledException(): Initializes a new instance with the
system-supplied error message.
See that you can avoid passing a CancellationToken instance to initialize
a new instance of OperationCanceledException Or, you can instantiate
it with a different token. In such cases, you get the final status as Faulted
(instead of Canceled).

To experiment with this, let’s create a new token, say token2, and update the
if block in Demonstration 8 as follows:

// Approach-2
if (token.IsCancellationRequested)
{
WriteLine("Cancelling the print activity.");
// Do some cleanups, if required
throw new OperationCanceledException(token2);
}

Execute the program again. Notice that the final status appears as Faulted,
but not Canceled. Here is a sample for you:

Simple cancellation demonstration.


Enter c to cancel the task.
0
1
2
3
c
Task cancellation requested.
Cancelling the print activity.
The final status of printTask is: Faulted
End of the main thread.

Q&A Session

Q2.5 Why does the output show the final status Faulted instead of
Canceled?

This is a design decision. The documentation states the following


(https://learn.microsoft.com/en-us/dotnet/standard/parallel-
programming/task-cancellation):
If the token’s IsCancellationRequested property returns false
or if the exception’s token doesn’t match the Task's token, the
OperationCanceledException is treated like a normal exception,
causing the Task to transition to the Faulted state. The presence of
other exceptions will also cause the Task to transition to the Faulted
state.

Monitoring Task Cancellation

Have you noticed the output of Demonstration 7 and Demonstration 8? In


both cases, you saw the following line: Cancelling the print activ-
ity.. I used this line to monitor the canceled task before the cancellation
operation. Interestingly, there are alternatives. Let’s see some of them.

Using Register

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 18/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
You can subscribe to an event notification. For example, in the following code, I
register a delegate that will be called when the token is canceled:

token.Register(
() =>
{
WriteLine("Cancelling the print activity.
[Using event subscription]");
// Do something else, if you want
}
);

Using WaitHandle.WaitOne

Let me show you one more approach that is relatively complicated compared to
the previous one. However, this will also give you an idea about how to monitor
task cancellation. The documentation describes WaitHandle’s WaitOne method
as follows (see https://learn.microsoft.com/en-
us/dotnet/api/system.threading.waithandle.waitone?view=net-
8.0):
Blocks the current thread until the current WaitHandle receives a
signal.

The WaitOne method has many overloads. In the upcoming demonstration,


I’ll show you the simplest form that does not require you to pass any argument.
The basic idea is that the current thread will consider a token and wait until
someone cancels it. As soon as someone invokes the cancellation, the blocking
function call will be released. This is why I can launch another task from the
calling thread as follows:

Task.Run(
() =>
{
token.WaitHandle.WaitOne();
WriteLine("Cancelling the print activity.
[Using WaitHandle]");
// Do something else, if you want
}
);

Notice that it is similar to subscribing to an event notification, because


here also you wait for the cancellation to occur. This is why I have writ-
ten a similar statement in this code block.

Demonstration 9

It is time for another demonstration where I show you two approaches to


monitor the cancellation operation. Notice the key changes in bold:

using static System.Console;


WriteLine("Monitoring the cancellation operation.");
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
token.Register(
() =>
{
WriteLine("Cancelling the print activity.[Using event
subscription]");
// Do something else, if you want
}
);

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 19/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
var printTask = Task.Run
(
() =>
{
// A loop that runs 100 times
for (int i = 0; i < 100; i++)
{
// Approach-3
token.ThrowIfCancellationRequested();
WriteLine($"{i}");
// Imposing the sleep to make some delay
Thread.Sleep(500);
}
}, token
);
Task.Run(
() =>
{
token.WaitHandle.WaitOne();
WriteLine("Cancelling the print activity.[Using
WaitHandle]");
// Do something else, if you want
}
);
WriteLine("Enter c to cancel the task.");
char ch = ReadKey().KeyChar;
if (ch.Equals('c'))
{
WriteLine("\nTask cancellation requested.");
tokenSource.Cancel();
}
// Wait till the task finishes the execution
while (!printTask.IsCompleted) { }
WriteLine($"The final status of printTask is: {printTask.Status}");
WriteLine("End of the main thread.");

Output

Here is some sample output. Notice the changes in bold.

Monitoring the cancellation operation.


Enter c to cancel the task.
0
1
2
c
Task cancellation requested.
Cancelling the print activity.[Using WaitHandle]
Cancelling the print activity.[Using event subscription]
The final status of printTask is: Canceled
End of the main thread.

Cancelling Child Tasks

I assume you understand task cancellations now. I’d like to remind you
that Demonstration 8 in Chapter 1 showed you how to cancel a child task
to answer Q1.8. That is why I won’t repeat the discussion here.

Q&A Session

Q2.6 I understand that to provide support for cancellation, I need to


create a CancellationTokenSource instance and get a token from it.

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 20/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

However, I may need to support several cancellation strategies in a


program. How can I implement the concept?

Good question. You may need to work with several tokens. The upcoming
demonstration will give you the idea.

Managing Multiple Cancellation Tokens

An application can indeed be canceled due to various reasons. In such a


case, you can use multiple tokens and provide the necessary logic. In this
context, you can use the CreateLinkedTokenSource method. Let’s see an
example.

In the following demonstration, you’ll see two different tokens as follows:

var normalCancellation = new CancellationTokenSource();


var tokenNormal = normalCancellation.Token;
var unexpectedCancellation = new CancellationTokenSource();
var tokenUnexpected = unexpectedCancellation.Token;

Once the tokens are created, I pass them to the CreateLinkedTokenSource


method as follows:

var compositeToken = CancellationTokenSource.


CreateLinkedTokenSource(tokenNormal,tokenUnexpected);

The idea is that you can cause a cancellation using either normalCancel-
lation or unexpectedCancellation.

You may note that the CreateLinkedTokenSource method has different


overloads, and you can pass more tokens if required. Remember that the
core idea is the same: you can cancel any of these tokens to make the final
task status Canceled.

Demonstration 10

In the following program, a user can trigger a normal cancellation. But you can
also observe an unexpected/emergency cancellation as well. To mimic an
emergency cancellation, I rely on a generated random number. If the number is 5,
the unexpected cancellation will be triggered. Here is the complete program to
demonstrate the idea:

using static System.Console;


WriteLine("Monitoring the cancellation operation.");
var normalCancellation = new CancellationTokenSource();
var tokenNormal = normalCancellation.Token;
var unexpectedCancellation = new CancellationTokenSource();
var tokenUnexpected = unexpectedCancellation.Token;
tokenNormal.Register(
() =>
{
WriteLine(" Processing a normal cancellation.");
// Do something else, if you want
}
);
tokenUnexpected.Register(
() =>
{
WriteLine(" Processing an unexpected cancellation.");

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 21/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
// Do something else, if you want
}
);
var compositeToken = CancellationTokenSource.CreateLinkedTokenSource(
tokenNormal,
tokenUnexpected
);
var printTask = Task.Run
(
() =>
{
// A loop that runs 100 times
for (int i = 0; i < 100; i++)
{
compositeToken.Token.ThrowIfCancellationRequested();
WriteLine($"{i}");
// Imposing sleep to make some delay
Thread.Sleep(500);
}
}, compositeToken.Token
);
int random = new Random().Next(1, 6);
// A dummy logic to mimic an emergency cancellation
if (random == 5)
unexpectedCancellation.Cancel();
else
{
WriteLine("Enter 'c' for a normal cancellation ");
char ch = ReadKey().KeyChar;
if (ch.Equals('c'))
{
WriteLine("\nTask cancellation requested.");
normalCancellation.Cancel();
}
}
// Wait till the task finishes the execution
while (!printTask.IsCompleted) { }
WriteLine($"The final status of printTask is: {printTask.Status}");
WriteLine("End of the main thread.");

Output

Here is some sample output after a user presses C to mimic a normal


cancellation:

Monitoring the cancellation operation.


Enter 'c' for a normal cancellation
0
1
2
c
Task cancellation requested.
Processing a normal cancellation.
The final status of printTask is: Canceled
End of the main thread.

Here is some more sample output when an emergency cancellation was


triggered:

Monitoring the cancellation operation.


0
Processing an unexpected cancellation.
The final status of printTask is: Canceled

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 22/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
End of the main thread.

Microsoft’s Note for Visual Studio Users

If you are a Visual Studio user and write programs that deal with multiple
cancellation requests, I want you to remember the following note from Microsoft
(see https://learn.microsoft.com/en-
us/dotnet/standard/threading/how-to-listen-for-multiple-
cancellation-requests):
When “Just My Code” is enabled, Visual Studio in some cases will
break on the line that throws the exception and display an error mes-
sage that says “exception not handled by user code.” This error is be-
nign. You can press F5 to continue from it.

It keeps saying the following:


To prevent Visual Studio from breaking on the first error, just
uncheck the “Just My Code” checkbox under Tools, Options,
Debugging, General.

Organizing Exceptions and Cancellations

In Chapter 1, you learned that there are three possible final states of a
task: RanToCompletion, Canceled, and Faulted. In this chapter, you
learned about cancellations and exceptions (particularly, we talked about
AggregateException and OperationCanceledException). However, up
until now, I discussed exceptions and cancellations in separate sections.
This is why typical try-catch blocks were absent when I discussed can-
cellations. But typical software must be prepared for exceptions and can-
cellations. So, it is time to consolidate all this knowledge in a single place.
Let’s analyze some case studies.

Case Study 1: Using Wait()

The following program simulates a money transfer process. It also shows


the progress status in the output. To simplify everything, let’s assume that
a money transfer takes five seconds. So, after every second, I increase the
progress status by 20%.

In the upcoming program, there is a try block followed by two catch


blocks. Why? Instead of catching all exceptions in a single catch block, I’d
like to use separate catch blocks to handle the possible scenarios of ex-
ceptions and cancellations. One of these catch blocks handles
AggregateException, and another catch block handles
OperationCanceledException.

Demonstration 11

Here is the complete program:

using static System.Console;


WriteLine("Handling cancellations and exceptions.");
CancellationTokenSource cts = new();
CancellationToken token = cts.Token;
var transferMoney = Task.Run(
() =>
{
WriteLine($"Initiating the money transfer.");
int progressBar = 0;

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 23/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
WriteLine("Press c to cancel within 5 sec.");
// Assuming the task will take 5 seconds.
// So, after every second, we'll increase
// the progress by 20%
for (int i = 0; i < 5; i++)
{
token.ThrowIfCancellationRequested();
Thread.Sleep(1000);
progressBar += 20;
WriteLine($"Progress:{progressBar}%");
}
return "The money transfer is completed.";
}, token);
var input = ReadKey().KeyChar;
if (input.Equals('c'))
{
WriteLine("\nCancellation is requested.");
cts.Cancel();
}
try
{
transferMoney.Wait();
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
WriteLine($"Caught error: {e.Message}");
return true;
});
}
catch (OperationCanceledException oce)
{
WriteLine($"Caught error due to cancellation: {oce.Message}");
}
if (transferMoney.Status == TaskStatus.RanToCompletion)
{
WriteLine(transferMoney.Result);
}
WriteLine($"Current status: {transferMoney.Status}");
WriteLine("Thank you, visit again!");

Output

Let me show you some possible outputs. Notice the important lines in
bold in these outputs.

Case 1: In this case, you do not cancel the task. Here is some sample output:

Handling cancellations and exceptions.


Initiating the money transfer.
Press c to cancel within 5 sec.
Progress:20%
Progress:40%
Progress:60%
Progress:80%
Progress:100%
The money transfer is completed.
Current status: RanToCompletion
Thank you, visit again!

Case 2: Here is some sample output from the program when you try to cancel
the task:

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 24/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

Handling cancellations and exceptions.


Initiating the money transfer.
Press c to cancel within 5 sec.
Progress:20%
Progress:40%
Progress:60%
c
Cancellation is requested.
Progress:80%
Caught error: A task was canceled.
Current status: Canceled
Thank you, visit again!

Case Study 2: Using Wait(token)

Let’s replace the try block in the previous program with the following one:

// There is no change in the previous code


try
{
//transferMoney.Wait();
transferMoney.Wait(token);
// There is no change in the remaining code

Let’s execute this modified program and analyze the previous cases again.
First, if you do not cancel the task, you will not observe any change in the output.
However, if you initiate a cancellation, there are some differences. Let me show you
some sample output:

Handling cancellations and exceptions.


Initiating the money transfer.
Press c to cancel within 5 sec.
Progress:20%
Progress:40%
Progress:60%
c
Cancellation is requested.
Caught error due to cancellation: The operation was canceled.
Current status: Running
Thank you, visit again!

You can see that this time the catch block for AggregateException could
not catch OperationCanceledException; this is why I used two different
catch blocks in this program.

Q&A Session

Q2.7 Why does the previous output show the final status Running in-
stead of Canceled?

The Wait(token) differs from Wait(). In the case of Wait(token), the


wait terminates if a cancellation token is canceled before the task is completed. In
this case, the main thread exited early. So, to see the final status of the task as
Canceled, you can introduce the following code (shown in bold) in the following
location:

// There is no change in the previous code.


// Wait till the task finishes the execution
while (!transferMoney.IsCompleted) { }
WriteLine($"Current status:{transferMoney.Status}");

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 25/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
// There is no change in the remaining code.

Here is some sample output after this change:

Handling cancellations and exceptions.


Initiating the money transfer.
Press c to cancel within 5 sec.
Progress:20%
Progress:40%
Progress:60%
c
Cancellation is requested.
Caught error due to cancellation: The operation was canceled.
Progress:80%
Payment processing status: Canceled
Thank you, visit again!

POINT TO NOTE
When you examine this in detail, you’ll see that Wait() can throw only
AggregateException, whereas Wait(CancellationToken cancella-
tionToken) is cancellable and can raise OperationCanceledException.
If interested, you can see a discussion of this topic at
https://stackoverflow.com/questions/77833724/why-the-catch-
block-of-aggregateexception-was-not-sufficient-to-handle-
cancellat/77833858#77833858.

Handling I/O-Bound Tasks

In addition to running the essential tasks, sometimes you’ll want to run


an additional task to monitor the overall activity. At a later stage, you may
provide a choice: whether the user likes to retrieve the details of the ac-
tivity. In a traditional scenario, a user supplies some specific input(s) to
know the details. You may think that this is a common activity. Wait!
There is something more to add: since it purely depends on the user’s
choice, you would like to run this additional task without blocking the
current thread. Can you design such an application? The following sec-
tion will show you an implementation strategy.

Using TaskCompletionSource

You can consider using the TaskCompletionSource<TResult> class to


implement the idea. Using it you can a create task out of any operation that will
be completed in the future. Microsoft describes this class as follows (see
https://learn.microsoft.com/en-
us/dotnet/api/system.threading.tasks.taskcompletionsource-1?
view=net-8.0):
Represents the producer side of a Task<TResult> unbound to a dele-
gate, providing access to the consumer side through the Task prop-
erty.

More specifically, behind the scenes, using this class, you get a “slave”
task that you can manually drive when the operation finishes (or faults).
It can be ideal for such a type of I/O-bound work where you reap all the
benefits of using a task without blocking the calling thread.

Now the question is: how do you use this class? First, you need to instantiate
it. Once you instantiate this class, you will get some built-in methods that can
serve your purpose. Figure 2-4 shows the details of this class in Visual Studio.

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 26/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

Figure 2-4 The TaskCompletionSource class details


Before you see the code examples, I suggest that you note the following
points:

Figure 2-4 confirms that the method names start either with Set or
with TrySet. The first category returns void, and the second category
returns bool.
When you call any of these methods, the task moves into any of the
final states: RanToCompletion, Faulted, or Canceled.
You should call the first category (i.e., where the method names
start with the word Set) exactly once; otherwise, you’ll see exceptions.

To illustrate the previous bullet point, let me give you an example: in the
upcoming program, there is a custom class called Job. I created an instance of it,
called job, and used it in the task definition as follows (notice that the
SetResult method is called exactly once):

var backgroundTask = Task.Run(


() =>
{
// Some other codes before this line are not shown
// to focus on the main point of discussion
// taskCompletionSource.SetResult(job);
// The following line will cause an exception now
// taskCompletionSource.SetResult(job);
});

Now if you uncomment the commented-out line in the previous code and wait
for this task to finish (for example, using the line backgroundTask.Wait();),
you will see the following exception in the final output:

Unhandled exception. System.AggregateException: One or more errors occurred. (An attempt was mad
// The remaining details are not shown

To avoid this kind of error, I prefer to use the TrySetResult method in-
stead of the SetResult method. The reason is obvious: it returns a
Boolean, i.e., either true or false.

NoteIn the Demo12_UsingTaskCompletionSource project, I kept the erro-


neous code block as commented code for your
experimentation/reference. You can download it from the Apress
website.
Demonstration 12

To understand the following demonstration, go through the following points:

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 27/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

There is a Job class in this program. To create an instance of it, I pass a


job identification number and the name of the executor. Passing the
executor’s name was optional for me.
At the beginning of the program, I created an instance of the
TaskCompletionSource<Job> class, called taskCompletionSource. As
a result, I can use its Task property in a later stage.
The user of this application can get the details of the created Job in-
stance by entering the character y to get the details. In such a case, the
result will be collected through a task called backgroundTask that
completes taskCompletionSource.Task.
If the user enters any other character, the program completes execu-
tion without showing the job details.
Let’s see the complete program now:

using static System.Console;


TaskCompletionSource<Job> taskCompletionSource = new();
Task<Job> collectInfoTask = taskCompletionSource.Task;
WriteLine($"Starts processing a job.");
Job job = new(1)
{
Executor = "Roy"
};
// Do something else if required
// Starting a background task that will complete
// taskCompletionSource.Task
var backgroundTask = Task.Run(
() =>
{
WriteLine(" Monitoring the activity before setting the result.");
// Imposing some forced delay before setting the
// result to mimic real-world
Thread.Sleep(3000);
bool setResultStatus= taskcompletionSource.TrySetResult(job);
});
// Imposing a forced delay so that the background task can
// start running before executing the rest of the code
Thread.Sleep(1000);
WriteLine("Press 'y' to get the details.");
var input = ReadKey();
if (input.KeyChar == 'y')
{
WriteLine(collectInfoTask.Result);
// Same as:
// WriteLine(taskCompletionSource.Task.Result);
}
WriteLine("\nThank you!");
internal class Job
{
public int JobNumber { get; init; }
public string Executor { get; set; }
public Job(int jobNumber, string executor = "Anonymous")
{
JobNumber = jobNumber;
Executor = executor;
}
public override string ToString() =>
$"\n{Executor} executed the job number {JobNumber}";
}

Output

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 28/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
Here is some sample output:

Case-1( The user opt for the detail):


Starts processing a job.
Monitoring the activity before setting the result.
Press 'y' to get the details.
y
Roy executed the job number 1
Thank you!
Case-2( The user does not opt for the detail):
Starts processing a job.
Monitoring the activity before setting the result.
Press 'y' to get the details.
n
Thank you!

Q&A Session

Q2.8 I can see that you have used a property and a constructor to in-
stantiate a Job instance. Is there any specific reason behind this
design?

Yes. You can indeed opt for any of them. But notice that I have used the init
accessor in this example. About this accessor, Microsoft says the following (see
https://learn.microsoft.com/en-us/dotnet/csharp/language-
reference/keywords/init):
In C# 9 and later, the init keyword defines an accessor method in
a property or indexer. An init-only setter assigns a value to the prop-
erty or the indexer element only during object construction. This en-
forces immutability, so that once the object is initialized, it can't be
changed again.

This is why the job number cannot be changed in this program, but its ex-
ecutor can be reassigned. Fixing the job number for a particular job, I
promote immutability. In a multithread environment, promoting im-
mutability is a good idea.

Q2.9 How does immutability fit into a multithreaded programming


environment?

Immutable types are thread-safe. They prevent nasty bugs from implicit
dependencies, destructive updates, and state mutations. Therefore, in a
multithreaded environment, they make your programming life easy.

In addition, immutable types can help you avoid side effects and tempo-
ral coupling as well. Indeed, I am not focusing on functional program-
ming in this book. So, for the simple demonstrations, I do not care about
them. I simply wanted you to keep these notes in your mind using this
program.

Exercises

Check your understanding by attempting the following exercises:

REMINDER
As said before for Chapter 1’s exercises, for all code examples, the
Implicit Global Usings setting was enabled in Visual Studio. This is why
you will not see me mentioning some other namespaces that were avail-

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 29/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

able by default. You can safely assume that all other necessary name-
spaces are available for these code segments. The same comment applies
to all exercises in this book as well.

E2.1 If you execute the following code, can you predict the output?

using static System.Console;


WriteLine("Exercise 2.1");
try
{
int b = 0;
Task<int> value = Task.Run(() => 10 /b);
}
catch (Exception e)
{
WriteLine($"Caught error: {e.Message}");
}
WriteLine("End");

E2.2 If you execute the following code, can you predict the output?

using static System.Console;


WriteLine("Exercise 2.2");
try
{
int b = 0;
Task<int> value = Task.Run(() => 10 / b);
WriteLine(value.Result);
}
catch (Exception e)
{
WriteLine($"Encountered with {e.GetType()}");
}
WriteLine("End");

E2.3 If you execute the following code, can you predict the output?

using static System.Console;


WriteLine("Exercise 2.3");
try
{
var task1 = Task.Run(
() => throw new InvalidDataException("invalid data"));
var task2 = Task.Factory.StartNew(
() => throw new OutOfMemoryException("insufficient
memory"));
Task.WaitAll(task1, task2);
}
catch (AggregateException ae)
{
foreach (Exception e in ae.InnerExceptions)
{
WriteLine($"Caught error: {e.Message}");
}
}

E2.4 If you execute the following code, can you predict the output?

using static System.Console;


WriteLine("Exercise 2.4");
try

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 30/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
{
var task1 = Task.Run( () => throw new
InvalidDataException("invalid data"));
var task2 = Task.Run(() => throw new
OutOfMemoryException("insufficient memory"));
task1.Wait();
task2.Wait();
WriteLine("End");
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
if (e is InvalidDataException |
e is OutOfMemoryException )
{
WriteLine($"Caught error: {e.Message}");
return true;
}
return false;
}
);
}

E2.5 If you execute the following code, can you predict the output?

using static System.Console;


WriteLine("Exercise 2.5 and Exercise 2.6");
try
{
DoSomething();
}
catch (AggregateException ae)
{
ae.Handle(
e =>
{
WriteLine($"Caught inside main: {e.Message}");
return true;
}
);
}
static void DoSomething()
{
try
{
var task1 = Task.Run(() => throw new
InvalidDataException("invalid data"));
var task2 = Task.Run(() => throw new
OutOfMemoryException("insufficient memory"));
// For Exercise 2.5
Task.WaitAll(task1, task2);
// For Exercise 2.6
// task1.Wait();
// task2.Wait();
}
catch (AggregateException ae)
{
ae.Handle(
e =>
{
if (e is InvalidDataException)
{
WriteLine($"Caught inside DoSomething:
{e.Message}");

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 31/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
return true;
}
else
{
return false;
}
}
);
}
}

E2.6 Predict the output when you replace the following line:

Task.WaitAll(task1, task2);

with the following lines in the previous program:

task1.Wait();
task2.Wait();

E2.7 If you execute the following code, can you predict the output?

using static System.Console;


WriteLine("Exercise 2.7");
try
{
var task1 = Task.Run(() => throw new
InvalidOperationException("invalid operation"));
var task2 = Task.Run(() => throw new
OutOfMemoryException("insufficient memory"));
Task.WaitAny(task1, task2);
WriteLine("End");
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
if (e is InvalidOperationException |
e is OutOfMemoryException
)
{
WriteLine($"Caught error: {e.Message}");
return true;
}
return false;
}
);
}

E2.8 If you execute the following code, can you predict the output?

using static System.Console;


WriteLine("Exercise 2.8");
try
{
var someTask = Task.Run(() => throw new
InvalidOperationException("invalid operation")
{ Source = "Task1"});
// Allowing some time so that task can get up and running
while (someTask.Status != TaskStatus.Running)
{
Thread.Sleep(10);

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 32/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
}
// Waiting for the state change now
while (someTask.Status == TaskStatus.Running)
{
Thread.Sleep(10);
}
WriteLine($"SomeTask's status: {someTask.Status}");
WriteLine("The application ends here.");
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
if (e is InvalidOperationException )
{
WriteLine($"Caught error: {e.Message} Source=
{e.Source}");
return true;
}
return false;
}
);
}

E2.9 If you execute the following code, can you predict the output?

using static System.Console;


var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var printTask = Task.Run
(
() =>
{
int i = 0;
while (i != 10)
{
if (token.IsCancellationRequested)
{
WriteLine("Cancelling the print activity.");
return;
}
// Do some work, if required.
Thread.Sleep(1000);
i++;
}
}, token
);
Thread.Sleep(500);
WriteLine("Task cancellation initiated.");
tokenSource.Cancel();
// Wait till the task finishes the execution
while (!printTask.IsCompleted) { }
WriteLine($"The final status of printTask is: {printTask.Status}");
WriteLine("End of the main thread.");

E2.10 In the previous exercise, if you replace the following code segment:

if (token.IsCancellationRequested)
{
WriteLine("Cancelling the print activity.");
return;
}

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 33/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
with the following line:

token.ThrowIfCancellationRequested();

will be there any change in the output?

E2.11 The following program creates a parent task and a nested task. It also
allows you to cancel the nested task if you enter C quickly. Check whether you can
predict the output.

using static System.Console;


WriteLine("Exercise 2.11");
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var parent = Task.Factory.StartNew(
() =>
{
var finalResult = string.Empty;
try
{
if (token.IsCancellationRequested)
{
finalResult = "The cancellation request is raised before the start of the parent
token.ThrowIfCancellationRequested();
}
WriteLine("The parent task has started.");
// Creating a nested task
var child = Task.Factory.StartNew(
() =>
{
WriteLine("The nested task has started.");
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
WriteLine($"\tThe nested task
prints:{i}");
Thread.Sleep(100);
}
return "The nested task has finished too.";
}, token);
finalResult = "Parent task: completed. Additional
info: n/a.";
// Updating the final result with the result of the
// nested task
finalResult = $"Parent task: completed.\t Additional info: {child.Result} ";
}
catch (AggregateException ae)
{
foreach (Exception e in ae.InnerExceptions)
{
WriteLine($"Caught error: {e.Message}");
}
}
catch (OperationCanceledException oce)
{
WriteLine($"Error: {oce.Message}");
}
return finalResult;
});
WriteLine("Enter c to cancel the nested task.");
char ch = ReadKey().KeyChar;
if (ch.Equals('c'))
{

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 34/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
WriteLine("\nTask cancellation requested.");
tokenSource.Cancel();
}
WriteLine($"Status: {parent.Result}");
WriteLine("End of the main thread.");

E2.12 If you execute the following code, can you predict the output?

using static System.Console;


TaskCompletionSource<int> tcs = new();
int value = 10;
var task1=Task.Run(() => value++);
Thread.Sleep(1000);
var task2=Task.Run(() =>
{
Thread.Sleep(2000);
tcs.SetResult(value*10);
}
);
Thread.Sleep(1000);
WriteLine($"The final result is:{tcs.Task.Result}");

E2.13 In Chapter 1 you solved exercise 1.6. Since you learned about
implementing exception and cancellation scenarios, can you solve
that exercise considering these scenarios?

Summary

This chapter continued the discussion of task programming, but this time the
focus was on handling various special scenarios with different examples and case
studies. In brief, it answered the following questions:

How do you handle exceptions in a parallel programming


environment?
How can you support cancellations in a parallel programming
environment?
How can you monitor cancellations in your applications?
How can you manage some I/O-bound tasks using the
TaskCompletionSource class?

Solutions to Exercises

Here is a sample solution set for the exercises in this chapter.

E2.1

This program produces the following output:

Exercise 2.1
End

Additional Note: You do not observe the exception because the main
thread did not encounter the exception; it was encountered by the task
that was created by this main thread. I covered this topic using
Demonstration 2 and Demonstration 3.

E2.2

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 35/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
This time you’ll see the exception name. You may note that instead of seeing
System.DivideByZeroException, you’ll see System.AggregateException
as follows:

Exercise 2.2
Encountered with System.AggregateException
End

E2.3

This program produces the following output:

Exercise 2.3
Caught error: invalid data
Caught error: insufficient memory

E2.4

The call to the statement task1.Wait(); causes the InvalidDataException.


As a result, control leaves the try block and produces the following output:

Exercise 2.4
Caught error: invalid data

E2.5

The program produces the following output:

Exercise 2.5 and Exercise 2.6


Caught inside DoSomething: invalid data
Caught inside main: insufficient memory

E2.6

The program produces the following output (see the explanation of E2.4 if
required):

Exercise 2.5 and Exercise 2.6


Caught inside DoSomething: invalid data

E2.7

This program produces the following output:

Exercise 2.7
End

You may be wondering why are you not seeing the task’s exception(s) in the
output. This is because when you use the WaitAny method, the task’s exception
does not propagate to the AggregateException. I encourage you to read
Stephen Clearly’s blog post at
https://blog.stephencleary.com/2014/10/a-tour-of-task-part-5-
wait.html that summarizes the difference among WaitAny, Wait, and WaitAll
as follows:
The semantics of WaitAny are a bit different than WaitAll and
Wait: WaitAny merely waits for the first task to complete. It will not

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 36/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…

propagate that task’s exception in an AggregateException. Rather,


any task failures will need to be checked after WaitAny returns.
WaitAny will return -1 on timeout, and will throw
OperationCanceledException if the wait is cancelled.

E2.8

This program may produce the following output:

Exercise 2.8
SomeTask's status: Faulted
The application ends here.

However, you may also notice that the program is stuck executing. Why?
Notice that when you evaluate the following condition, the task may not be in the
Running state (for example, it may be in the WaitingForActivation state):

// Allowing some time so that the task can get up and running
while (someTask.Status != TaskStatus.Running)
{
Thread.Sleep(10);
}

But inside the loop when you invoke the Sleep method, by that time, the
task may transition from the Running state to one of the terminating
states. As a result, the program will stuck here.

This is why to get a consistent output, you’d like to replace the following
segment:

// Allowing some time so that task can get up


// and running( Program can stuck here)
while (someTask.Status != TaskStatus.Running)
{
Thread.Sleep(10);
}
// Waiting for the state change now
while (someTask.Status == TaskStatus.Running)
{
Thread.Sleep(10);
}

with the following one: while (!someTask.IsCompleted) { }

NoteI already mentioned that you should avoid this kind of polling in the
production code as it is inefficient.

E2.9

Here is some possible output:

Task cancellation initiated.


Cancelling the print activity.
The final status of printTask is: RanToCompletion
End of the main thread.

E2.10

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 37/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
This time the final task status should appear as Canceled. Here is some sample
output:

Task cancellation initiated.


The final status of printTask is: Canceled
End of the main thread.

E2.11

You already know that this program creates a parent task and a nested task. It
also allows you to cancel the nested task if you enter C quickly. As a result, you
may see different output. For example, if you do not intend to cancel the nested
task and press the Enter key at the end, you can see the following output:

Exercise 2.11
Enter c to cancel the nested task.
The parent task has started.
The nested task has started.
The nested task prints:0
The nested task prints:1
The nested task prints:2
The nested task prints:3
The nested task prints:4
The nested task prints:5
The nested task prints:6
The nested task prints:7
The nested task prints:8
The nested task prints:9
Status: Parent task: completed. Additional info: The nested task has finished too.
End of the main thread.

On the other hand, depending on the time of cancellation, you can get
different outputs. For example, if you press C almost at the beginning of the
application, you will see the following:

Exercise 2.11
Enter c to cancel the nested task.
c
Task cancellation requested.
Error: The operation was canceled.
Status: The cancellation request is raised before the start of the parent task.
End of the main thread.

Otherwise, you may see something like the following:

Exercise 2.11
Enter c to cancel the nested task.
The parent task has started.
The nested task has started.
The nested task prints:0
The nested task prints:1
The nested task prints:2
The nested task prints:3
c
Task cancellation requested.
Caught error: A task was canceled.
Status: Parent task: completed. Additional info: n/a.
End of the main thread.

E2.12

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 38/39
2024/10/31 晚上9:06 2. Handling Special Scenarios | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony B…
You should see the following output:

The final result is: 110

[Clue: Notice that task1 updates the initial value to 11, but task2 further
sets it as 11*10=110. The sleep statements are placed to preserve the or-
der of evaluation.]

E2.13

I leave this exercise to you now. Good luck!

https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_2_Chapter.xhtml 39/39

You might also like