Task Programming in C# and .NET
Task Programming in C# and .NET
Programming
in C# and .NET
Modern Day Foundation for
Asynchronous Programming
—
Vaskaran Sarcar
Apress Pocket Guides
Apress Pocket Guides present concise summaries of cutting-edge
developments and working practices throughout the tech industry. Shorter
in length, books in this series aims to deliver quick-to-read guides that are
easy to absorb, perfect for the time-poor professional.
This series covers the full spectrum of topics relevant to the modern
industry, from security, AI, machine learning, cloud computing, web
development, product design, to programming techniques and business
topics too.
Typical topics might include:
• A concise guide to a particular topic, method, function
or framework
Vaskaran Sarcar
Task Programming in C# and .NET: Modern Day Foundation for
Asynchronous Programming
Vaskaran Sarcar
Kolkata, West Bengal, India
vii
TABLE OF CONTENTS
viii
TABLE OF CONTENTS
Summary.............................................................................................................89
Exercises .............................................................................................................90
Solutions to Exercises .........................................................................................94
ix
TABLE OF CONTENTS
Index .................................................................................................161
x
About the Author
Vaskaran Sarcar obtained his Master of
Engineering degree in Software Engineering
from Jadavpur University, Kolkata (India),
and an MCA from Vidyasagar University,
Midnapore (India). He was a National Gate
Scholar (2007–2009) and has over 12 years of
experience in education and the IT industry.
He devoted his early years (2005–2007) to the
teaching profession at various engineering
colleges, and later, he joined HP India PPS R&D Hub in Bangalore. He
worked there until August 2019 and became a Senior Software Engineer
and Team Lead. After working for more than ten years at HP, he decided to
follow his passion completely. He is now an independent full-time author.
You can refer to the link amazon.com/author/vaskaran_sarcar (or
Appendix B) to find all his books. You can also find him on LinkedIn at
https://www.linkedin.com/in/vaskaransarcar.
xi
About the Technical Reviewer
Shekhar Kumar Maravi is a software
architect – design and development, whose
main interests are programming languages,
Linux system programming, Linux kernel,
algorithms, and data structures. He obtained
his master’s degree in Computer Science
and Engineering from Indian Institute of
Technology Bombay. After graduation, he
joined Hewlett Packard's R&D Hub in India
to work on printer firmware. Currently, he is a
Product and Solution Development Team Lead for automated pathology
lab diagnostic devices at Siemens Healthcare R&D division. He can
be reached by email at [email protected] or via LinkedIn at
https://www.linkedin.com/in/shekharmaravi.
xiii
Acknowledgments
At first, I thank the Almighty. I sincerely believe that with His blessings
only, I could complete this book. I extend my deepest gratitude and thanks
to the following people:
Shekhar: Whenever I was in need, he provided
support. He answered all my queries over phone
calls, WhatsApp, and emails. Thank you one
more time.
Smriti, Kripa, Celestin, and the Apress team: I
sincerely thank each of you for giving me another
opportunity to work with you and Apress.
Nirmal and the Production team: Thanks to each
of you for your exceptional support in beautifying
my work. Your efforts are extraordinary.
Finally, I thank those people from different online communities
(particularly, the C# developer community, .NET developer community,
and Stack Overflow community) who share their knowledge in various
forms. In fact, I thank everyone who directly or indirectly contributed to
this work.
xv
Introduction
With the availability of multicore computers, asynchronous programming
and parallel programming are becoming increasingly important. Why not?
It is essential for building highly responsive software.
This is why playing with threads in a multithreaded environment is
inevitable. Undoubtedly, it is hard, but in earlier days, it was harder. To
simplify the overall coding experience, starting from the .NET Framework
4.0, Microsoft introduced Task Parallel Library (TPL) which was based on
the concept of tasks. Later, in C#5, we saw the revolutionary introduction
of the async and await keywords. Using them, we started passing the
heavy work(s) to the compiler. However, you need to remember that a
typical async method normally returns a task (in programming terms, a
Task or a Task<TResult>). So, there is no wonder that task programming
became the modern-day foundation for asynchronous programming. In
addition, the patterns used earlier to deal with asynchronous and parallel
programming are not recommended now.
This is why I decided to write a pocketbook series on asynchronous
and parallel programming. This pocketbook series will try to simplify
the concept using the modern C# features and libraries that Microsoft
recommends. Task Programming in C# and .NET: Modern Day Foundation
for Asynchronous Programming is the first book in this series. It focuses on
task programming without using the async and await keywords.
xvii
INTRODUCTION
xviii
INTRODUCTION
Prerequisite Knowledge
I expect you to be very much familiar with C#. In fact, knowing about
some of the advanced concepts like delegates and lambda expressions
can accelerate your learning. I assume that you know how to compile or
run a C# application in Visual Studio. This book does not invest time in
easily available topics, such as how to install Visual Studio on your system
xix
INTRODUCTION
or how to write a “Hello World” program in C#. In short, the target readers
of this book are those who want to make the most out of C# by harnessing
the power of both object-oriented programming (OOP) and functional
programming (FP).
xx
INTRODUCTION
Useful Software
These are the important software/tools that I used for this book:
• All the programs were tested with C# 13 and .NET 9.
In this context, it is useful to know that nowadays the
C# language version is automatically selected based
on your project's target framework(s) so that you can
always get the highest compatible version by default.
In the latest versions, Visual Studio doesn't support
the UI to change the value, but you can change it by
editing the csproj file. If you are interested more in
the C# language versioning, you can follow the link
https://docs.microsoft.com/en-us/dotnet/csharp/
language-reference/configure-language-version.
• During the development of this book, software updates
kept coming, and I also kept updating. When I finished
my initial draft, I had the latest edition of Microsoft
Visual Studio Community 2022 (64-bit) – Preview
Version 17.12.0 Preview 3.0. When I submitted the final
draft, I had Microsoft Visual Studio Community 2022
(64-bit)-17.12.4.
• The good news for you is that this community edition
is free of cost. If you do not use the Windows operating
system, you can also use Visual Studio Code which
is also a source code editor developed by Microsoft
that runs on Windows or macOS and Linux operating
systems. This multiplatform IDE is also free. However,
I recommend that you check the license and privacy
statement as well. It is because this statement may
change in the future.
xxi
INTRODUCTION
Author's note: I have tested my code only on Visual Studio. You may note
that “Visual Studio 2022 for Mac” was already scheduled for retirement
by August 31, 2024. To know more on this, you can refer to the online
link https://learn.microsoft.com/en-us/visualstudio/mac/what-
happened-to-vs-for-mac?view=vsmac-2022.
• This book suits you best if you are familiar with some
advanced features in C# such as delegates and lambda
expressions. If not, please read those topics before you
start reading this book.
• I believe that sequential reading of these chapters
can help you learn faster. So, I suggest you go through
the chapters sequentially. Another reason for this
suggestion is that some useful and related topics may
have already been discussed in a previous chapter,
and I have not repeated those discussions in the later
chapters.
• The programs in this book should give you the
expected output in the upcoming versions of C#/Visual
Studio as well. Though I believe that these results
should not vary in other environments, you know the
nature of software: it is naughty. So, I recommend that
if you want to see the same output, it will be better if
you can mimic the same environment.
• You can download and install the Visual Studio
IDE from https://visualstudio.microsoft.com/
downloads/. And you are expected to get Figure 1.
xxii
INTRODUCTION
Figure 1. The download link for Visual Studio 2022 and Visual
Studio Code
Note At the time of this writing, this link works fine, and the
information is correct. However, the link and policies may change in the
future. The same comment applies to all the mentioned links in this book.
xxiii
INTRODUCTION
xxiv
INTRODUCTION
Final Words
You are an intelligent person. You have chosen a topic that can assist
you throughout your career. As you learn and review these concepts, I
suggest you write your code; only then will you master this area. There is
no shortcut for this. Do you know the ancient story of Euclid and Ptolemy,
ruler of Egypt? Euclid’s approach to mathematics was based on logical
reasoning and rigorous proofs, and Ptolemy asked Euclid if there was an
easier way to learn mathematics. Euclid’s reply to the ruler? "There is no
royal road to geometry."
Though you are not studying geometry, the essence of this reply
applies here. You must study these concepts and code. Do not give
up when you face challenges. They are the indicators that you are
growing better.
Errata: I have tried my level best to ensure the accuracy of the content.
However, mistakes can happen. So, I have a plan to maintain the “Errata,”
and if required, I can also make some updates/announcements there. So,
I suggest that you visit those pages to receive any important corrections or
updates.
An Appeal: You can easily understand that any good quality work takes
many days and many months (even years!). Many authors like me invest
most of their time in writing and heavily depend on it. You can encourage
and help these authors by preventing piracy. If you come across any illegal
copies of our works in any form on the Internet, I would be grateful if you
would provide me/the Apress team with the location address or website
name. In this context, you can use the link https://www.apress.com/gp/
services/rights-permission/piracy as well.
xxv
INTRODUCTION
Share Your Feedback: I believe that the book is designed for you in such
a way that upon its completion, you will develop an adequate knowledge
of parallel programming using C# and .NET. So, I hope that you will value
the effort. Once you finish reading this book, I request you to provide
your valuable feedback on the Amazon review page or any other platform
you like.
xxvi
CHAPTER 1
Asynchronous
Programming and
Tasks
Starting with an introduction to asynchronous programming, this chapter
quickly covers tasks and their importance.
Useful Scenarios
Every scenario is not suitable for asynchronous programming. For
example, if the kids are naughty, probably, a mother cannot do things
asynchronously. In this case, she may decide to first complete the
homework before she enters the kitchen (or vice versa). This means that
she needs to opt for synchronous executions only.
2
CHAPTER 1 ASYNCHRONOUS PROGRAMMING AND TASKS
You may argue by saying that to reduce the work pressure, the working
mother can appoint a tutor to teach her kids or a cook to prepare breakfast
or meals. If this is the case, let me tell you that in such cases, she needs to
increase her expenses as well. However, if there is a budget constraint, she
may need to do all the work by herself. So, you can see that every scenario
is not suitable for asynchronous executions (or parallel operations).
Now the question is: when should you exercise asynchronous
programming? Here are some typical scenarios in which you can benefit
from asynchronous programming:
• To fulfill the I/O bound needs such as accessing
a database, reading or writing to a large file, and
requesting data through the Internet
• To fulfill the CPU-bound needs such as performing a
complex and time-consuming calculation
Q&A Session
Q1.1 It appears to me that asynchronous programming can increase
the code complexity. Is this not a problem?
True. However, consider the case in which to make a more responsive
UI, you offload a long-running operation to a background thread. As a
result, you do not need to block the main UI thread to avoid frustrating
user experience. This is appreciable because, in the end, you are making
the users happy.
Q1.2 Are asynchronous and parallel programming the same?
Let us first consider some real-world examples. Suppose, you start
preparing rice for dinner. Typically, after cleaning the rice, you add
water and start boiling the combination over low-medium heat. Now,
you need to wait until the water is absorbed. In between, you decide to
give some tasks to your kids. So, you come back from the kitchen and
instruct your kids to do their homework. This is similar to asynchronous
3
CHAPTER 1 ASYNCHRONOUS PROGRAMMING AND TASKS
programming because you can go back to the kitchen and again come
back to your kids until you complete these works. However, it is not
equivalent to parallel programming.
Now, consider a situation when you appoint a cook to make the dinner
for your family. In this case, the cook can prepare the rice, and you can
focus on your kid's homework at the same time. In this case, two different
people are doing two different tasks at the same time. This is equivalent to
parallel programming with a dual-core machine.
You understand that by combining these models, you can reap
benefits. For example, multiple people can be engaged in preparing the
meal (consider a cook with a helper), and multiple people can teach
your kids (each tutor can focus on one kid). We try to do this exactly
in programming. However, the terms asynchronous programming
and parallel programming are pretty close and related to each
other. The Visual Studio Magazine summarized this by saying (see
https://visualstudiomagazine.com/articles/2011/03/24/wccsp_
asynchronous-programming.aspx):
Asynchronous programming is a means of parallel
programming in which a unit of work runs separately from
the main application thread and notifies the calling thread of
its completion, failure or progress.
Programming Patterns
Undoubtedly, asynchronous programming is hard, but in earlier days, it
was harder. In those days, developers had the following options:
• Direct use of threads
• Direct use of ThreadPool class
• Use of callback methods
4
CHAPTER 1 ASYNCHRONOUS PROGRAMMING AND TASKS
Recommended Pattern
Then, what is the recommended pattern? See the official link https://
learn.microsoft.com/en-us/dotnet/standard/asynchronous-
programming-patterns/task-based-asynchronous-pattern-tap that
states the following:
In .NET, the task-based asynchronous pattern is the
recommended asynchronous design pattern for new
development. It is based on the Task and Task<TResult> types
in the System.Threading.Tasks namespace, which are used to
represent asynchronous operations.
However, this book does not discuss task-based asynchronous
pattern (TAP) in detail. This is because most of the TAP implementations
involve the async and await keywords which will be discussed in another
pocketbook in this learning series. This book is purely focused on task
programming without using them. Understanding these topics will
immensely help when you deep dive into asynchronous programming and
start applying async and await in your programs.
5
CHAPTER 1 ASYNCHRONOUS PROGRAMMING AND TASKS
For now, it will be sufficient for you to understand that TPL simplifies
the multithread scenarios and helps you write high-performance code
without worrying about the nitty-gritty of threading or other low-level
details. The following statement from Microsoft (see https://learn.
6
CHAPTER 1 ASYNCHRONOUS PROGRAMMING AND TASKS
microsoft.com/en-us/dotnet/standard/parallel-programming/task-
parallel-library-tpl ) nicely summarizes the usefulness of TPL:
The purpose of the TPL is to make developers more productive
by simplifying the process of adding parallelism and
concurrency to applications.
Introducing Tasks
TPL is based on the concept of tasks. So, the question is: what is a task? You
can consider a task as a code block that represents a unit of work. You can
inform the scheduler that this code block can execute on a separate thread
while the main thread can continue its execution. For example, consider
the following code segment:
Useful Scenarios
The following list includes some common scenarios where you can
use tasks:
• Performing a calculation and displaying the result
• Computing a value with/without a supplied input
7
CHAPTER 1 ASYNCHRONOUS PROGRAMMING AND TASKS
Q&A Session
Q1.3 What are the benefits of using tasks over threads?
Here are some common benefits:
• The tasks are relatively lightweight. They help you
achieve fine-grained parallelism.
• Later, you’ll see that by using the in-built API for tasks,
you easily exercise useful operations such as waiting,
cancellations, continuations, custom scheduling, or
robust exception handling. So, when you opt for tasks
instead of threads, you’ll have more programmatic
control.
Q1.4 What were the fundamental advances in task over the previous
programming models?
There are many benefits. Once you complete this book, these benefits
will be apparent to you. For now, you may note that while using tasks,
after you initiate the operation(s), you can easily connect the producer of
the task and consumer(s) of the task by providing a continuation of work.
More specifically, you do not need to provide the continuation work to
the method that invokes the operation. It is one of the primary benefits of
using tasks over the previous programming models.
8
CHAPTER 1 ASYNCHRONOUS PROGRAMMING AND TASKS
Summary
This chapter started with a discussion on asynchronous programming and
its usage in the current world. Then, it introduced TPL and the role of tasks in
asynchronous programming. In brief, it answered the following questions:
• What is asynchronous programming and why is it
important?
• What are the ideal scenarios for asynchronous
programming?
• What is TPL?
Exercises
Check your understanding by attempting the following exercises:
E1.1 State True/False:
i) In .NET, the task-based asynchronous pattern is the
current recommended pattern for asynchronous
programming.
ii) Asynchronous executions are always faster than the
corresponding synchronous executions.
9
CHAPTER 1 ASYNCHRONOUS PROGRAMMING AND TASKS
Solutions to Exercises
Here is a sample solution set for the exercises in this chapter.
E1.1
The answers are shown inline in bold:
i) In .NET, the task-based asynchronous pattern is the
current recommended pattern for asynchronous
programming. [True]
ii) Asynchronous executions are always faster than the
corresponding synchronous executions. [False]
10
CHAPTER 1 ASYNCHRONOUS PROGRAMMING AND TASKS
E1.2
The following list includes two primary benefits of using an asynchronous
application:
11
CHAPTER 2
Task Creation
and Execution
This chapter helps you dive into task programming. Here, you’ll learn
different ways to create and execute tasks. Once you execute a task, you
may be interested to see the result. It means that you need to wait for
the task completion. This is why this chapter discusses different kinds of
waiting mechanisms as well.
Given the following code, let me show you the common approaches for
task creation and execution:
Approach-1:
The Task.Run method is the recommended and most common to create
and start a task. This approach helps you automatically start the task after
creation. Here is a sample code:
Approach-2:
To create a task and automatically start the execution, you can use the
Task.Factory.StartNew method as well. Using this approach, you can
exercise advanced options to configure tasks. Here is a sample code:
Approach-3:
In addition, you can use the Task constructor to create a task. However, in
this case, you need to explicitly start it by calling the Start method. Here is
a sample code:
Note You have seen the approaches that explicitly create and
execute tasks. There are other approaches as well. For example,
you can implicitly create and execute tasks using Parallel.Invoke
method. In addition, TaskCompletionSource<TResult> class also
helps you create specialized tasks that are suitable for particular
scenarios. However, let us learn the concepts one at a time.
14
CHAPTER 2 TASK CREATION AND EXECUTION
Demonstration 1
Let us verify whether tasks help us perform asynchronous programming
using the following demonstration:
POINT TO REMEMBER
.NET 6 onward, you may notice the presence of implicit global directives for
new C# projects. This helps you use the types in these namespaces without
specifying the fully qualified names or manually adding a “using directive.” You
can learn more about this from the online link https://learn.microsoft.
com/en-us/dotnet/core/project-sdk/overview#implicit-using-
directives.
For the C# projects in this book, I did not change the default settings. As a
result, you will not see me mentioning the following namespaces that were
available by default:
15
CHAPTER 2 TASK CREATION AND EXECUTION
System
System.Collections.Generic
System.IO
System.Linq
System.Net.Http
System.Threading
System.Threading.Tasks
Task.Run(PrintNumbers);
16
CHAPTER 2 TASK CREATION AND EXECUTION
Output
Here is a sample output of this program from my computer. It is obvious
that the output can vary in your system. You can see that the main thread
was not blocked during the execution of the printing task. There is a nice
mixture of output from all the different threads/tasks.
Q&A Session
Q2.1 To create and execute tasks you have shown me the use of
Run, Start, and StartNew methods. How can I decide which one is
best for me?
If you see the method definitions in Visual Studio, you will see the
following statement: “The Start method starts the System.Threading.
Tasks.Task, scheduling it for execution to the specified System.
Threading.Tasks.TaskScheduler.” This method is useful when you
manually execute the task depending on some condition.
The Run method queues the specified work to run on the thread pool
and returns a System.Threading.Tasks.Task object that represents that
work. This is a lightweight alternative to the StartNew method. It helps you
start a task with the default values. This indicates that the Run method uses
the default task scheduler, regardless of a task scheduler that is associated
with the current thread. This is why Microsoft (see https://learn.
microsoft.com/en-us/dotnet/standard/parallel-programming/task-
based-asynchronous-programming) provides the following suggestions:
• The Run methods are the preferred way to create and
start tasks when more control over the creation and
scheduling of the task isn’t needed.
17
CHAPTER 2 TASK CREATION AND EXECUTION
Microsoft further says (see the same link) that you can use the
StartNew method for the following situations (see the same link):
• Creation and scheduling don’t have to be separated,
and you require additional task creation options or the
use of a specific scheduler.
• You need to pass an additional state into the task that
you can retrieve through its Task.AsyncState property.
POINT TO NOTE
To give you a specific example, let me tell you that you’ll learn about
child tasks (or nested tasks) shortly. There you’ll see that using
TaskCreationOptions.AttachedToParent, you can attach a child
task to the parent task (if the parent task allows this activity). This option is
available in some of the overloads of the StartNew method. However, the Run
method does not provide a similar option.
18
CHAPTER 2 TASK CREATION AND EXECUTION
Now, if you run the program again, you’ll see the similar output that
you already saw by running Demonstration 1.
I assume that I do not need to remind you that you can replace the
previous line of code with the following line:
19
CHAPTER 2 TASK CREATION AND EXECUTION
This constructor gives you the idea that you can pass an object
argument to them. So, let me introduce another function called
PrintNumbersVersion2 that takes an object parameter and does a similar
thing. It is as follows (notice the changes in bold):
20
CHAPTER 2 TASK CREATION AND EXECUTION
Or
However, the Run method does not have any such constructor. So, you
cannot write something like
Ok, you have already seen many different approaches while passing a
state so far. Let us summarize them:
// Approach-1:
var task1 = Task.Run(() => PrintNumbers(10));
// Approach-2:
var task2=Task.Factory.StartNew(() => PrintNumbers(10));
// Approach-3:
var task3 = new Task(() => PrintNumbers(10));
task3.Start();
// Approach-4:
var task4 = new Task(PrintNumbersVersion2,10);
task4.Start();
// Approach-5:
var task5 = Task.Factory.StartNew(PrintNumbersVersion2,10);
You can see that in each approach, I passed an int. However, in the last
two cases, the target method (PrintNumbersVersion2) expected an object.
As a result, these two approaches suffer from the impact of boxing and
unboxing. On the contrary, they look cleaner compared to approaches 1, 2,
or 3. In the end, it is up to you how you want to organize it.
21
CHAPTER 2 TASK CREATION AND EXECUTION
However, the type argument can be inferred, and as a result, you can
further simplify this code as follows:
POINT TO NOTE
In the previous code segments, I have used the var type to type less. Here is
an equivalent code that explicitly shows the types:
22
CHAPTER 2 TASK CREATION AND EXECUTION
Demonstration 2
Let us see a complete program that deals with some tasks, passes some
values into them, and finally, retrieves the computed result.
In the following demonstration, I create a task that adds two integers
(10 and 20). Once the task is completed, I retrieve the computed result and
display it in the console window. Let us see the complete program now:
WriteLine($"{firstNumber}+{SecondNumber}={result}");
WriteLine($"The main thread is completed.");
Output
Here is the output:
23
CHAPTER 2 TASK CREATION AND EXECUTION
Q&A Session
Q2.2 I can see that in the previous demonstration, you did not use the
ReadKey method to prevent the end of the console mode application.
Was it intentional?
When you want to get a result from a task, you need to wait until the
task finishes its execution. It means that you need to invoke a blocking
operation. Using the Result property, I did the same: I blocked the calling
thread until the invoked task finished its execution. As a result, I did not
need to use any more blocking constructs.
Author’s note: Task instances can use the Wait method as well for a task
to complete execution. Shortly, you’ll see a discussion on different kinds of
waiting mechanisms.
Q&A Session
Q2.3 I understand that using the blocking calls like var result = addTask.
Result; or addTask.Wait(); you effectively make the code synchronous.
Then, why did you show me the program that uses the blocking calls?
I wanted you to be aware of it. In addition, there will be situations
where you cannot proceed until you get a result from an intended task.
In those cases, you cannot avoid blocking calls (Demonstration 3 will
24
CHAPTER 2 TASK CREATION AND EXECUTION
give you an idea about this). So, if you consider using it, try to do so after
running other code that can run asynchronously. However, do not worry!
Shortly, you’ll see the discussion on nonblocking calls as well.
Discussion on Waiting
There are different built-in constructs for waiting. In this section, let me
show you the need for “waiting” and discuss some useful methods to
implement the idea.
Why Do We Wait?
Once you execute a task, you may like to get the result. It means that
you need to wait for the task to finish its execution. The following
demonstration gives you the idea.
Demonstration 3
In the following program, the calling thread (aka main thread) invokes
two different tasks. Let us execute the program and analyze some of the
possible outputs:
25
CHAPTER 2 TASK CREATION AND EXECUTION
{
WriteLine("Wait for your lucky number...");
// Simulating a delay
Thread.Sleep(1);
WriteLine($"---Your lucky number is: {new Random().
Next(1,10)}");
}
);
Output
Here I include some possible outputs from my computer:
Output 1:
Output 2:
26
CHAPTER 2 TASK CREATION AND EXECUTION
Analysis
These outputs reflect the following characteristics:
How Do We Wait?
You understand that to see the final status of these tasks, you may need
to wait for some more time. How can you wait? There are different
approaches. Let me show you some of them in the following section.
Using Sleep
Probably, one of the simplest solutions is to block the main thread until
other tasks are finished. Here is a sample where I block the main thread for
1000 milliseconds:
27
CHAPTER 2 TASK CREATION AND EXECUTION
The advantage of using this approach is obvious. We can see that when
the main thread sleeps, other tasks could execute their job. It indicates that
during sleep, the scheduler can schedule other tasks.
On the contrary, this approach has an obvious problem: you may block
the thread unnecessarily for some additional time. For example, I can
see a similar output (I am saying “similar” instead of “same” because the
generated random number keeps varying which is an expected behavior
for this program) in my computer if I block the main thread for 500
milliseconds or less too. However, the problem is that since we cannot
predict the exact time for these tasks to be completed, I need to block
it for a reasonable amount of time. So, if any of these tasks take more
time to complete due to some other factors, you may not see the task’s
completion message in the output. This is a problem for sure!
Neither we want unnecessary waiting nor do we want to miss any key
information. From this point of view, it is an inefficient approach. In fact, the
situation can be worse if you work on an application that tries to block the
UI. This is why relying on the Sleep method may not always be a good idea.
Using Delay
In the previous code, let me replace the statement, Thread.Sleep(1000);
with Task.Delay(1000); in the main thread as follows:
28
CHAPTER 2 TASK CREATION AND EXECUTION
and run the program again. Again, my computer shows different possible
outputs, and one of them is as follows:
This output reflects that the calling thread was not blocked for the
printLuckyNumberTask and processOrderTask to complete their job. So,
you see the line “The end of main.” before the order was processed or
the lucky number was displayed. This gives you a clue that you should
use Sleep for the synchronous pauses, whereas you should prefer the
Delay method for nonblocking delays. This is why the use of the Delay
method can help you build a more responsive UI.
In fact, Visual Studio IDE will give you a clue about this. Let me explain
this: Once you learn more about asynchronous programming, you’ll
know that the use of async and await keywords simplifies asynchronous
programming. Then, you may write something like the following: await
Task.Delay(1000);. However, without using the await keyword, if you use
the line Task.Delay(1000); you’ll see the following warning message:
CS4014 Because this call is not awaited, execution of the current
method continues before the call is completed. Consider apply-
ing the ‘await’ operator to the result of the call.
29
CHAPTER 2 TASK CREATION AND EXECUTION
While using the Delay method, you can also assign it to a task and “await” at a
later point in time as follows:
Using Wait
Demonstration 2 showed that by using the Result property, you can block the
calling thread until the specific task is finished. However, it is not necessary that
in every scenario, you analyze the outcome of task execution. In fact, a task may
not return a value as well. So, let me show you another waiting technique.
30
CHAPTER 2 TASK CREATION AND EXECUTION
Invoking the Wait method on a Task instance, you can wait for it to
complete. Here is a sample where I call Wait on printLuckyNumberTask
and processOrderTask separately:
Using WaitAll
Instead of waiting for the individual tasks to be completed, you can wait for
a group of tasks. In such cases, you use the WaitAll method and provide
the task objects for which you want to wait as parameters. Here is a sample:
This change can also produce an output where you’ll see that both
tasks finished their executions.
Using WaitAny
Suppose there are multiple tasks, but you’d like to wait for any of them to
complete. In such cases, you use the WaitAny method as follows:
31
CHAPTER 2 TASK CREATION AND EXECUTION
This output shows that this time the main thread did not wait for the
processOrderTask to finish its execution.
POINTS TO NOTE
Using WhenAny
Notice the previous output once again. You can see that the line “The end
of main.” came after at least one of the tasks finished its execution. If you
execute the program repeatedly, you’ll never see that the mentioned line
appears before at least one task finishes its execution. It is because, in the
case of WaitAny, the calling thread is blocked till any of those tasks
finishes the execution. Interestingly, there is another method, called
WhenAny, that does not block the calling thread.
Consider the following code where I replace WaitAny with WhenAny:
32
CHAPTER 2 TASK CREATION AND EXECUTION
You can see that the main thread was not blocked in this case.
Q&A Session
Q2.4 In some blogs/articles, I see the usage of Thread.SpinWait, instead
of Thread.Sleep. How do they differ?
The SpinWait method is useful for implementing locks but not for
ordinary applications. When you use spin waiting, the scheduler does
not pass the control to some other task, which means it avoids context
switching. The online link https://learn.microsoft.com/en-us/dotnet/
api/system.threading.thread.spinwait?view=net-9.0 states:
In the rare case where it is advantageous to avoid a context
switch, such as when you know that a state change is imminent,
make a call to the SpinWait method in your loop. The
code SpinWait executes is designed to prevent problems that
can occur on computers with multiple processors. For example,
on computers with multiple Intel processors employing
Hyper-Threading technology, SpinWait prevents processor star-
vation in certain situations.
33
CHAPTER 2 TASK CREATION AND EXECUTION
It is useful to note that you can use the SpinUntil method as well. At
the time of this writing, there are three overloaded versions of this method:
SpinUntil(Func<Boolean>)
SpinUntil(Func<Boolean>, Int32)
SpinUntil(Func<Boolean>, TimeSpan)
Let me show you a usage of the simplest version that spins until the
specified condition is fulfilled. Here is a sample for you where I wait until
printLuckyNumberTask completes its execution properly:
34
CHAPTER 2 TASK CREATION AND EXECUTION
POINT TO NOTE
The key takeaway is that there are different ways of waiting. You can use the
one which is more convenient for you. I am mentioning only those methods
that will be sufficient for you to understand the rest of this book. Remember
that these methods have various overloaded versions as well.
35
CHAPTER 2 TASK CREATION AND EXECUTION
Summary
This chapter gave you a quick overview of task creations and executions. It
also described different waiting mechanisms for task completions. In brief,
it answered the following questions:
• What is a task and how can you create a task?
• How can you pass values into tasks?
• How can you return a value from a task?
• How can you employ a waiting mechanism in task
programming?
Exercises
Check your understanding by attempting the following exercises:
POINT TO REMEMBER
As said before, for all code examples, the “Implicit Global Usings” was enabled
in Visual Studio. This is why you’ll not see me mentioning the following
namespaces that were available by default:
System
System.Collections.Generic
System.IO
System.Linq
System.Net.Http
System.Threading
System.Threading.Tasks
The same comment applies to all exercises in this book as well.
36
CHAPTER 2 TASK CREATION AND EXECUTION
E2.1 If you execute the following code, can you predict the output?
37
CHAPTER 2 TASK CREATION AND EXECUTION
Solutions to Exercises
Here is a sample solution set for the exercises in this chapter.
E2.1
Notice that you have created the task but you have not started this task. So,
the program will output the following:
End.
E2.2
The program can show more than one possible output. However, notice
that you did not wait for the task to finish its execution. So, depending on
the computer’s speed, the order of the output statements can vary. Here is
one sample output:
It is also possible that in the output, you see “How are you doing?”
only if the task takes some more time to start. To examine this, you can
run the following code in which by introducing some more delay inside
welcomeTask (see the bold line), I increase the probability of finishing the
main thread early:
38
CHAPTER 2 TASK CREATION AND EXECUTION
E2.3
C#12 allows you to define default values for parameters on lambda
expressions. So the code can be compiled without any issues. In addition,
this time the output is also predictable because the main thread must wait
for the task to finish. So, you will see the following output:
Hello, reader!
Goodbye.
E2.4
Here is a sample solution that calculates the factorial of 10 using a
background task:
39
CHAPTER 2 TASK CREATION AND EXECUTION
E2.5
The answers are shown inline in bold:
40
CHAPTER 3
Continuation
and Nested Tasks
This chapter will give you an overview of task continuations, nested tasks,
and related topics.
Continuation Tasks
Suppose, there are two tasks, called Task A and Task B. If you want to start
executing Task B only after Task A, probably you’d like to use callbacks.
But TPL makes it easy. It provides the functionality through a continuation
task which is nothing but an asynchronous task. The idea is the same:
once an antecedent task finishes, it invokes the next task that you want to
continue. In our example, Task A is the antecedent task and Task B is
the continuation task. Let me summarize the important characteristics of
a continuation task:
• A continuation task is invoked by another task. It can
start when the prior task (i.e., the antecedent task) is
completed. It means continuations are nothing but
chaining tasks.
• Using this concept, you can pass data (as
well as exceptions) from an antecedent to the
continuation task.
Simple Continuation
Assume that a person, named Jack, wants to invite his friends to a dinner
party. At a high level, let us divide the overall activity into two different
tasks as follows:
• Inviting friends
• Ordering food
Let us assume that Jack first invites his friends over the phone. Once
the invitation is done, he has an idea of how many people are joining the
42
Chapter 3 Continuation and nested tasks
party. Based on that, now he orders the food. You can see that inviting
friends is the antecedent task and ordering food is the continuation task.
Let us develop a program to mimic the scenario.
For the continuation task, you’ll notice the use of the ContinueWith
method. This method creates a continuation that executes when the
target task is completed. There are many overloads of this method. In
this example, I use the simplest version of ContinueWith that accepts an
Action<Task> as the parameter. This is why you will see the following
code block:
POINT TO NOTE
to show you the complete output, i use the ReadLine method in this example
to prevent the end of the console mode application. the same comment
applies whenever you see me using the ReadKey or ReadLine method in the
demonstrations of this book.
43
Chapter 3 Continuation and nested tasks
Demonstration 1
Here is the complete demonstration:
Output
Here is a sample output for you:
44
Chapter 3 Continuation and nested tasks
Analysis
The previous output confirms the following characteristics:
• You can also see that the continuation task started after
the antecedent task finished. In addition, it successfully
processed the data that was returned from the parent/
antecedent task.
• In this example, when the continuation task processed
the line WriteLine(previousTask.Result); it was
nonblocking. Why? Since the previous task was already
completed, its result was instantly available.
Conditional Continuations
Demonstration 1 shows you a simple continuation example. However, you
can have more control over the continuation process. Let us examine this
concept with some case studies.
Case Study 1
Even after inviting the guests, the host may need to shift the party due to
some unavoidable circumstances. In this case, instead of ordering the
food, let us assume that the host lets the guest know about the situation
and shifts the party date. Can you write a program to mimic the situation?
45
Chapter 3 Continuation and nested tasks
Surely, you can. However, let me show a technique that manages the
situation using the TaskContinuationOptions enumeration. The following
screenshot (see Figure 3-1) from Visual Studio shows you the different
members of TaskContinuationOptions:
46
Chapter 3 Continuation and nested tasks
Demonstration 2
Let us see the complete program now:
47
Chapter 3 Continuation and nested tasks
Output
Here is a possible output when there is no exception:
Here is another possible output where the host needed to shift the
party date:
48
Chapter 3 Continuation and nested tasks
Case Study 2
Task continuations help you deal with many different situations. Let
me show you one more case study on this topic. Earlier, I told you that
continuations can be attached to one or more antecedents. Let us see an
example.
This time, I’ll use the ContinueWhenAll method. As usual, there are
many overloads of this method. I am about to use the following one that
accepts only two parameters as follows:
49
Chapter 3 Continuation and nested tasks
POINT TO REMEMBER
You may note that the “Collection expressions” feature in C#12 allows us to
write arrangeDinnerTask in this way. if you are using an old version of C#,
you may need to write it as follows (notice the change in bold):
Demonstration 3
Let us see the complete program now:
ReadLine();
50
Chapter 3 Continuation and nested tasks
Output
Here is a possible output where food is ordered at the beginning:
Food is ordered.
Invitation is done.
Arranging dinner.
Invitation is done.
Food is ordered.
Arranging dinner.
Analysis
In every case, you can see that dinner has been arranged only after the task
of ordering food is completed and invitations are done.
Case Study 3
Let us analyze one more case study where you continue a task if any one
of the previous tasks completes the execution. In this case, you can use
ContinueWhenAny (instead of ContinueWhenAll). For example, here is
a sample output that I received when I replaced the ContinueWhenAll
method with the ContinueWhenAny method in the previous code:
Food is ordered.
Arranging dinner.
Invitation is done.
This output shows that dinner was arranged even before the invitations
were completed. You may increase the probability of seeing this output by
introducing a sleep statement inside inviteTask as follows:
51
Chapter 3 Continuation and nested tasks
POINT TO NOTE
CurrentId is used to get the identifier of the currently executing task from
the code that the task is executing. however, it is a static property, and it
differs from the Id property. Id returns the identifier of a particular Task
instance. attempting to retrieve the CurrentId value from outside the code
that a task is executing results a null return.
The life cycle of a Task instance passes through various stages. The
Status property is used to verify the current state. On investigation, you’ll
see that it returns TaskStatus which is an enum type and it has many
members. Let me take a snapshot from Visual Studio to show them (see
Figure 3-2).
52
Chapter 3 Continuation and nested tasks
• Canceled
• Faulted
As per their names, these states have their usual meaning. For
example, RanToCompletion indicates that the task was completed
successfully. Similarly, Faulted indicates the task completed due to an
unhandled exception. I assume that I need not mention that Canceled
53
Chapter 3 Continuation and nested tasks
indicates that a task is canceled that can occur due to various reasons such
as user intervention, timeouts, or any other application logic. I’ll discuss
more about exceptions and cancellations in Chapter 4 and Chapter 5.
Demonstration 4
In the following program, you’ll see two tasks. The first task
(doSomethingTask) can be completed successfully, or it can be
encountered with an exception. The second task (statusCheckerTask) is a
continuation task that checks the status of the parent task using the Status
property (it helps you retrieve the TaskStatus of the task). Here is the
complete program:
54
Chapter 3 Continuation and nested tasks
ReadKey();
Output
Here is a sample output:
Analysis
If needed, you can modify this program where you use separate branches
to handle different scenarios (similar to Demonstration 2). To illustrate the
previous line, let me replace the statusCheckerTask with the following blocks
of code that handle two possible branches (notice the key changes in bold)):
55
Chapter 3 Continuation and nested tasks
{
WriteLine($"The task {previousTask.Id}'s status is:
{previousTask.Status}");
}, TaskContinuationOptions.AttachedToParent |
TaskContinuationOptions.NotOnFaulted
);
Let’s run this modified program. This time, you’ll see separate
messages based on the task’s completion status. Here is the output when
the parent task encountered the exception:
Here is another output when the parent task did not encounter the
exception:
56
Chapter 3 Continuation and nested tasks
Q&A Session
Q3.1 In the previous outputs, I see the ID of the parent task is 7 or 8.
Looks like, many other tasks were also running along with it. Is this
correct?
Yes, I ran this code in VS2022 with the default settings in the debug
configuration where the hot reload was enabled. I asked the same question
at https://stackoverflow.com/questions/77726578/vs2022-versus-
vs2019-how-why-are-the-additional-tasks-being-created and
received the answer. If you run the same code in the release configuration
(or disable the “hot reload” setting), you can see the lower id as follows:
POINT TO REMEMBER
i often execute my programs in debug mode. so, to see the lower task ids
such as 1, 2, 3, and so forth in the output, i often run those programs with the
“hot reload” setting disabled.
57
Chapter 3 Continuation and nested tasks
Nested Tasks
Tasks can be nested. It means that you can create a task in the user
delegate of another task. The outer task in which the child task is created
is often referred to as the parent task. A child task can be any of the
following types:
• Attached: Created with the TaskCreationOptions.
AttachedToParent option (if the parent task allows this
to be attached)
• Detached: Executes independently
Demonstration 5
The following code creates two Task instances, called parent and child.
Notice that the child task is created inside the parent task. However,
I did not attach the child task to the parent. This is why you see the
line ,TaskCreationOptions.AttachedToParent is commented in the
following code.
POINT TO NOTE
58
Chapter 3 Continuation and nested tasks
Output
Here is a sample output from my computer:
You can see that this output reflects that the child task was started, but
it does not reflect whether it was finished. This is because I created the
child task without using TaskCreationOptions.AttachedToParent option.
59
Chapter 3 Continuation and nested tasks
Q&A Session
Q3.3 I understand that in the previous demonstration, you waited only
for the parent task to complete but not for the child task. So, if I replace
the line parent.Wait(); with the line Task.WaitAll(parent, child); I
can see whether the child task finishes its execution. Am I right?
No. In that code sample, the child task was nested. So, it was not in the
scope. So, your proposed code will cause the following compile-time error:
CS0103 The name 'child' does not exist in the current context
Q3.4 In the previous program, you used a Sleep statement in the main
thread. Was it necessary?
Nice catch. Indeed, it was not necessary. However, by placing this
Sleep statement, I increase the probability of showing the line "The child
task has started." in the output.
60
Chapter 3 Continuation and nested tasks
POINTS TO NOTE
You can attach a child task to the parent task if and only if the parent task
allows you to do this. in this context, you can note the following two points
from the official documentation (see https://learn.microsoft.com/
en-us/dotnet/standard/parallel-programming/attached-and-
detached-child-tasks):
1. parent tasks can explicitly prevent child tasks from attaching
to them by specifying the TaskCreationOptions.
DenyChildAttach option in the parent task's class
constructor or the TaskFactory.StartNew method.
2. parent tasks implicitly prevent child tasks from attaching to
them if they are created by calling the Task.Run method.
Q&A Session
Q3.5 I have a doubt: In the previous demonstration (Demonstration 5),
you have only written parent.Wait();. It means that you care about the
parent task to be finished, but the same is not true for the child task. As
a result, there is no guarantee that the output will reflect whether the
child task could finish its execution. Is this a correct understanding?
No. Microsoft has designed the architecture in such a way that if you
create a parent–child relationship, waiting on the parent task forces you to
wait for the child task to complete.
61
Chapter 3 Continuation and nested tasks
Demonstration 6
To demonstrate this, I have slightly modified the previous program. For
your reference, I have highlighted the new code in bold and commented
out the old code as follows:
}
);
// Thread.Sleep(5);
// parent.Wait();
// WriteLine($"The parent task has finished now.");
WriteLine($"The parent task confirms that {parent.Result}");
62
Chapter 3 Continuation and nested tasks
Output
Here is the output of this program:
WriteLine(someTask.Result);
63
Chapter 3 Continuation and nested tasks
POINTS TO NOTE
Once you execute this program, you’ll see the following output:
Received: 200
Interestingly, if you work with the Run method, it can do the same kind
of unwrapping for you. Here is an equivalent code:
64
Chapter 3 Continuation and nested tasks
Special Note
In this book, I did not discuss async and await keywords. However, in
this context, I’d like you to note that you can use the await keyword for
unwrapping a layer. For example, the following code will also compile and
produce the same output:
Summary
This chapter discussed task continuations and nested tasks. In brief, it
answered the following questions:
• How can you implement a simple task continuation
mechanism?
• How can you create branches to employ a conditional
continuation mechanism?
• How can you check the status of the current task?
• How can you create, manage, and unwrap a
nested task?
65
Chapter 3 Continuation and nested tasks
Exercises
Check your understanding by attempting the following exercises (you do
not need to handle exceptions or cancellations for these exercises):
REMINDER
as said before, you can safely assume that all other necessary namespaces
are available for these code segments. the same comment applies to all
exercises in this book as well.
Now, assume that there are two tasks where the first task will create an
Employee instance. The second task will follow the first task and perform
the following things: first, it will verify whether the first task completes the
process successfully. Next, it will print the current date and time. Can you
write a program fulfilling the criteria?
66
Chapter 3 Continuation and nested tasks
helloTask.Wait();
E3.5 Can you predict the following output of the following program?
67
Chapter 3 Continuation and nested tasks
Solutions to Exercises
Here is a sample solution set for the exercises in this chapter.
E3.1
Here is a sample program based on the features that you learned in this
chapter:
createEmp.Wait();
68
Chapter 3 Continuation and nested tasks
E3.2
Here is a sample program that fulfills the criteria:
69
Chapter 3 Continuation and nested tasks
Alternative Code:
Let me show you one more solution where you can avoid using the
variable statusTask and write a compact version of the previous program
as follows:
Author’s note: You can see that the main thread was not blocked while
executing the background task. It was blocked at the end for displaying the
output of the program. However, by this time, it completed its remaining
job. Once you download the project Chapter3_Ex3.2, you can exercise
both solutions.
E3.3
On my computer, this program often shows the following output:
Hello reader!
70
Chapter 3 Continuation and nested tasks
Hello reader!
How are you?
You may wonder about this. However, notice that to create the parent
task, I have used the Run method but not the StartNew method. So, if you
use the StartNew method instead of the Run method as follows:
Hello reader!
How are you?
Task.Factory.StartNew(
someAction,
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default);
71
Chapter 3 Continuation and nested tasks
E3.4
Yes. You should see the following output:
Received: 300
E3.5
Here is the output:
72
CHAPTER 4
Exception Handling
It is no surprise that tasks can encounter exceptions. It is also true that
different tasks may throw different exceptions. In a multithreaded
environment, handling these exceptions can be tricky as well as
challenging. Your application must respond to them gracefully to avoid
unwanted crashes and maintain stability. This is why exception handling is
essential for building reliable and robust applications. This chapter focuses
on this topic.
line that raises the exception. However, if you continue the execution (by
pressing F5 or the Continue button), you’ll not see any information about
this exception in the output.
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 (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 message that says
“exception not handled by user code.” This error is benign. You can press F5 to
continue from it.
Demonstration 1
Let us execute the program:
try
{
var validateUserTask = Task.Run(() =>
throw new UnauthorizedAccessException("Unauthorized user.")
);
}
74
CHAPTER 4 EXCEPTION HANDLING
catch (Exception e)
{
WriteLine($"Caught error: {e.Message}");
}
Output
Upon executing this program, you’ll see the following output:
The output does not show anything about the exception. Why? Notice
that in this program, the main thread did not encounter the exception;
it was encountered by validateUserTask which was created by this
main thread.
Since the unobserved exceptions can cause problems at a later stage,
you’d like to see and handle them as per the priority.
Introducing AggregateException
How could you get the information about the exception? An obvious way
is handling the exception inside the task itself. For example, I can redefine
the task as follows:
75
CHAPTER 4 EXCEPTION HANDLING
{
throw new UnauthorizedAccessException("Unauthorized
user.");
}
catch (Exception e)
{
WriteLine($"Caught error inside the task: {
e.Message} ");
}
return msg;
});
How can you get the error detail if the task does not handle the
exceptions? Let us investigate the answer.
Demonstration 2
In task-based programming, exceptions are stored in the task object and
are not thrown immediately when they occur. If an exception occurs within
a task, it is encapsulated within an AggregateException that contains all
the exceptions that were thrown during the task’s execution. This feature
allows you to handle the exceptions collectively or individually.
POINT TO NOTE
76
CHAPTER 4 EXCEPTION HANDLING
• You await the task. (Since this book does not discuss
async and await keywords, I am not discussing
this now.)
Now, you understand that in the previous demonstration if you
use any of the lines WriteLine(validateUserTask.Result); or
validateUserTask.Wait(); inside the try block, you can observe the
exception. Here is a sample demonstration where I use the statement
validateUserTask.Wait(); inside the try block as follows (notice the
change in bold):
try
{
var validateUserTask = Task.Run(() =>
throw new UnauthorizedAccessException("Unauthorized user.")
);
validateUserTask.Wait();
}
// There is no change in the remaining code as well
Output
Once you execute the program again, you will see the following output:
The output shows the error now. However, you can see that the error
information "Unauthorized user." is wrapped as an InnerException.
77
CHAPTER 4 EXCEPTION HANDLING
Q&A Session
Q4.1 In the previous output, the error information “Unauthorized
user.” is wrapped as an InnerException. What is the reason behind it?
This program caught an AggregateException which is used to
consolidate multiple failures into a single, throwable exception object. You
see this kind of exception heavily in task programming.
To verify this, you can slightly modify the catch block in the previous
demonstration (Demonstration 2) 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:
78
CHAPTER 4 EXCEPTION HANDLING
This is why you see the actual error (Unauthorized user.) was
wrapped as an inner exception in the previous output.
POINT TO NOTE
79
CHAPTER 4 EXCEPTION HANDLING
Demonstration 3
This program creates two different tasks inside the main thread. Each of
these tasks raises an exception. Here, I pass through the inner exceptions
and display the error details. Go through the complete program now:
try
{
var validateUserTask = Task.Run(
() =>
{
// Some other code, if any
throw new UnauthorizedAccessException("Unauthorized
user.");
}
);
var storeDataTask = Task.Run(
() =>
{
// Some other code, if any
throw new InsufficientMemoryException("Insufficient
memory.");
}
);
80
CHAPTER 4 EXCEPTION HANDLING
Task.WaitAll(validateUserTask, storeDataTask);
}
catch (AggregateException ae)
{
foreach (Exception e in ae.InnerExceptions)
{
WriteLine($"Caught error: {e.Message}");
}
}
Output
Here is a sample output from this program:
Alternative Approach-1
Notice the catch block in Demonstration 3. 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:
81
CHAPTER 4 EXCEPTION HANDLING
{
WriteLine($"Caught error: {e.Message}");
}
}
Alternative Approach-2
In the AggregateException class, you can see a method called Handle
which has the following form:
82
CHAPTER 4 EXCEPTION HANDLING
// Alternative approach-2
ae.Handle(e =>
{
WriteLine($"Caught error: {e.Message}");
return true;
});
}
By executing the program now, you can get the same output.
Note In the Chapter4_Demo3 project, you will see all the different
approaches that I discussed so far. There you’ll see that alternative
codes are commented out for easy comparison. By downloading the
project, you can play with these approaches.
Q&A Session
Q4.2 I can see that you have thrown two different exceptions from two
different tasks. How can I distinguish them?
It is easy. You can associate the task IDs or an appropriate message
with the exception’s Source property. Here is a sample program that
is created by slightly modifying Demonstration 3 (notice the changes
in bold):
try
{
var validateUserTask = Task.Run(
() =>
83
CHAPTER 4 EXCEPTION HANDLING
{
// Some other code, if any
throw new UnauthorizedAccessException("Unauthoriz
ed user.")
{ Source = "validateUserTask" };
}
);
var storeDataTask = Task.Run(
() =>
{
// Some other code, if any
throw new InsufficientMemoryException("Insufficient
memory.")
{ Source = "storeDataTask" };
}
);
Task.WaitAll(validateUserTask, storeDataTask);
}
// Initial approach
catch (AggregateException ae)
{
// Initial approach
foreach (Exception e in ae.InnerExceptions)
{
// WriteLine($"Caught error: {e.Message}");
WriteLine($"The task: {e.Source} raised {e.GetType()}:
{e.Message}");
}
}
84
CHAPTER 4 EXCEPTION HANDLING
85
CHAPTER 4 EXCEPTION HANDLING
{
if (e is InsufficientMemoryException)
{
WriteLine($"Caught error inside InvokeTasks():
{e.Message}");
return true;
}
else
{
return false;
}
}
);
}
Demonstration 4
Let us see a complete program now. In the upcoming program, the main
thread calls the InvokeTasks method that in turn creates and runs three
tasks. The first two tasks (validateUserTask and storeDataTask) are
already shown in the previous demonstration. You know that these tasks
will raise exceptions. The third task, named useDllTask, 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.
You’ll see that I catch all the possible sets of exceptions inside
InvokeTasks but handled only one of them: InsufficientMemoryException.
As a result, the remaining exceptions will be passed up to the calling
hierarchy. This is why you’ll see me handling them inside the main thread.
Let’s see the complete program now:
86
CHAPTER 4 EXCEPTION HANDLING
87
CHAPTER 4 EXCEPTION HANDLING
{
// Some other code, if any
throw new InsufficientMemoryException("Insufficient
memory.");
}
);
Task.WaitAll(validateUserTask, storeDataTask,
useDllTask);
}
// The catch block is placed here. To avoid repetitions,
// it is not shown again.
}
Note To avoid repetitions, I did not show you the catch block inside
the InvokeTasks method again. You can download the complete
program from the Apress website.
88
CHAPTER 4 EXCEPTION HANDLING
Output
Here is a sample output from this program:
Q&A Session
Q4.4 I understand that I can handle exceptions in different ways.
However, I’d like to know whether there is any general guideline for
handling exceptions in a concurrent environment.
Normally, experts suggest that if you do not handle exceptions within
tasks, you should try to handle them as closely as possible, particularly, to
those places where you wait for the task completion or retrieve the result of
the task invocation. I try to follow this guideline as well.
Summary
This chapter continued the discussion on task programming, but this time,
the focus was on handling exceptions with different examples and case
studies. In brief, it answered the following questions:
• What is AggregateException and why is it important in
task programming?
89
CHAPTER 4 EXCEPTION HANDLING
Exercises
Check your understanding by attempting the following exercises:
REMINDER
As said before, you can safely assume that all other necessary namespaces
are available for these code segments. The same comment applies to all
exercises in this book as well.
E4.1 If you execute the following code, can you predict the output?
90
CHAPTER 4 EXCEPTION HANDLING
E4.2 If you execute the following code, can you predict the output?
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 4.2
Task.WaitAll(task1, task2);
// For Exercise 4.3
// task1.Wait();
// task2.Wait();
}
91
CHAPTER 4 EXCEPTION HANDLING
Task.WaitAll(task1, task2);
task1.Wait();
task2.Wait();
92
CHAPTER 4 EXCEPTION HANDLING
try
{
int b = 0;
var task1 = Task.Run(() => throw new
InvalidOperationException("invalid operation"));
var task2 = Task.Run(() => 5/b);
Task.WaitAny(task1, task2);
WriteLine("End");
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
if (e is InvalidOperationException |
e is DivideByZeroException )
{
WriteLine($"Caught error: {e.Message}");
return true;
}
return false;
}
);
}
E4.5 Can you predict the following output of the following program?
93
CHAPTER 4 EXCEPTION HANDLING
Solutions to Exercises
Here is a sample solution set for the exercises in this chapter.
E4.1
This program produces the following output:
Exercise 4.1
End
Author’s 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.
To see the exception, you can modify the try block as follows (the
change is shown in bold):
94
CHAPTER 4 EXCEPTION HANDLING
Exercise 4.1
Caught: System.AggregateException, Message: One or more errors
occurred. (Attempted to divide by zero.)
End
E4.2
The program produces the following output:
E4.3
The call to the statement task1.Wait(); causes the
InvalidDataException. As a result, control leaves the try block and
produces the following output:
E4.4
This program produces the following output:
Exercise 4.4
End
95
CHAPTER 4 EXCEPTION HANDLING
You may be wondering why you are not seeing the task’s exception(s)
in the output. It 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 nice blog post (see https://blog.
stephencleary.com/2014/10/a-tour-of-task-part-5-wait.html) that
summarizes the difference between WaitAny and WaitAll (or Wait) 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 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.
Author’s note: Still, if you like to see the exception detail in the output,
you can slightly modify the try block as follows (the changes are shown
in bold):
96
CHAPTER 4 EXCEPTION HANDLING
Exercise 4.4
Caught error: invalid operation
E4.5
Here is the output:
Exercise 4.5
The status of the outer task is: RanToCompletion
The status of the inner task is: Faulted
97
CHAPTER 5
Managing
Cancellations
Cancellation is an essential mechanism in task programming. It is helpful
in any of the following scenarios:
• Stopping a running task gracefully when it is no
longer needed
Prerequisites
To manage task cancellations in C#, you must be familiar with the
following:
• CancellationTokenSource: This class is responsible
for signaling the cancellation. It generates a
CancellationToken that is passed to the task to
monitor the cancellation requests.
• CancellationToken: This is a struct that is passed to
the task and provides a way to check if cancellation has
been requested. This token is used to propagate the
notification for a task cancellation.
Let us see how to use them in your program. First, you use the
following lines of code:
Obviously, using the var keyword, you can write an equivalent code as
follows:
Next, you pass this token to the intended task. Earlier, you saw (in
Figure 2-1 in Chapter 2) that the Task constructor has several overloaded
versions. Some of them accept a CancellationToken instance as a method
parameter. Here is an example:
100
CHAPTER 5 MANAGING CANCELLATIONS
The StartNew method of the TaskFactory class and the Run method
of the Task class have similar overloads. Here are a few more examples for
your reference:
101
CHAPTER 5 MANAGING CANCELLATIONS
User-Initiated Cancellations
A cancellation request is often raised by users. You can also initiate an
automated cancellation after a certain time interval. Let us start our
discussion with the user-initiated cancellations.
Initial Approach
In the first approach, you evaluate an if condition before raising the
cancellation request. If needed, you can do some additional work before
you cancel the task. For example, you can introduce a message saying
that this task is about to be canceled. You can also clean up the necessary
resources as well before you cancel the task. Finally, you put a break or
return statement to exit from the particular block of code. Probably, most
of us are aware of this kind of soft exit mechanism. Let me show you an
example.
Demonstration 1
In the upcoming demonstration, I created a task that can keep printing the
numbers from 0 to 99. Since I’d like to provide support for cancellation,
you’ll see me instantiating a CancellationTokenSource object to generate
a cancellation token and pass it to the task to raise the cancellation
request. Here is the complete program:
102
CHAPTER 5 MANAGING CANCELLATIONS
Note 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.
WriteLine($"{i}");
// Imposing the sleep to make some delay
Thread.Sleep(500);
}
}, token
);
103
CHAPTER 5 MANAGING CANCELLATIONS
104
CHAPTER 5 MANAGING CANCELLATIONS
Output
Here is a sample output when I triggered the cancellation request by
pressing “c” from my keyboard:
Q&A Session
Q5.1 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 us see what Microsoft says about it. The online documentation
link https://learn.microsoft.com/en-us/dotnet/standard/parallel-
programming/task-cancellation states the following:
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 options:
• By returning from the delegate. In many scenarios,
this option is sufficient. However, a task instance that’s
canceled in this way transitions to the TaskStatus.
RanToCompletion state, not to the TaskStatus.Canceled state.
105
CHAPTER 5 MANAGING CANCELLATIONS
Alternative Approaches
Let us see the alternative ways of cancellations as well. In a similar
context, developers often like to throw the OperationCanceledException
exception.
Demonstration 2
To demonstrate this, let me update the task definition in the previous
demonstration as follows (notice the key change in bold):
106
CHAPTER 5 MANAGING CANCELLATIONS
{
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
);
Output
Here is a sample output when I triggered the cancellation request by
pressing “c” from my keyboard. Notice that in Demonstration 1, the final
status of the task was RanToCompletion, but in this demonstration, it
appears as Canceled.
107
CHAPTER 5 MANAGING CANCELLATIONS
if (token.IsCancellationRequested)
throw new OperationCanceledException(token);
WriteLine($"{i}");
// Imposing the sleep to make some delay
Thread.Sleep(500);
}
}, token
);
108
CHAPTER 5 MANAGING CANCELLATIONS
Q&A Session
Q5.2 In Demonstration 1, you simply did a soft exit and got the final
task status as RanToCompletion, whereas in Demonstration 2, the final
task status was canceled. I understand that this is a design decision, but
I’d like to know your thoughts on them.
Normally, I’d like to follow the approach that is shown in
Demonstration 2. This is because in an enterprise application, we
normally deal with several 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 will be no such record left for you.
if (token.IsCancellationRequested)
{
// Do some cleanups, if required
token.ThrowIfCancellationRequested();
}
109
CHAPTER 5 MANAGING CANCELLATIONS
110
CHAPTER 5 MANAGING CANCELLATIONS
// Approach-2
if (token.IsCancellationRequested)
{
WriteLine("Cancelling the print activity.");
// Do some cleanups, if required
// throw new OperationCanceledException(token);
throw new OperationCanceledException("The operation is
canceled.");
}
Execute the program again. Notice that the final status appears as
Faulted but not Canceled. Here is a sample for you:
111
CHAPTER 5 MANAGING CANCELLATIONS
Q&A Session
Q5.4 Why does the previous output show the final status Faulted
instead of Canceled?
It is a design decision. The online link https://learn.microsoft.
com/en-us/dotnet/standard/parallel-programming/task-
cancellation states:
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.
112
CHAPTER 5 MANAGING CANCELLATIONS
Now, execute the program again. This time, you’ll see that the
OperationCanceledException is not wrapped inside the
AggregateException. So, the catch block for the
OperationCanceledException was necessary to handle the exception.
Here is a sample output from my computer:
Q&A Session
Q5.5 Why does the previous output show the final status Running
instead of Canceled or Faulted?
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. To see the final status
113
CHAPTER 5 MANAGING CANCELLATIONS
of the task, you can introduce the following code (shown in bold) in the
following location:
POINTS TO NOTE
114
CHAPTER 5 MANAGING CANCELLATIONS
Timeout Cancellation
You can initiate a cancellation request after a specified time. For example,
for a typical network operation, you may not like to wait indefinitely. In
such a case, your program can automatically initiate the cancellation
request.
To implement the idea, you can use the line
tokenSource.CancelAfter(2000); in the previous demonstration
(Demonstration 2) as follows:
115
CHAPTER 5 MANAGING CANCELLATIONS
tokenSource.CancelAfter(2000);
// There is no change in the remaining code
Note Since I am not changing the remaining code, this program can
respond to user-initiated cancellations as well. In that case, the user
needs to raise this request before this automatic cancellation triggers.
In fact, it will wait for a user input before it closes the application. You
can download the project Chapter5_TimeoutCancellation to see the
complete program from the Apress website.
Using Register
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(
() =>
{
116
CHAPTER 5 MANAGING CANCELLATIONS
Using WaitHandle.WaitOne
Let me show you one more approach that is relatively complicated
compared to the previous one. However, this can also give you an idea
about how to monitor task cancellation. The online link https://learn.
microsoft.com/en-us/dotnet/api/system.threading.waithandle.
waitone?view=net-8.0 describes WaitHandle’s WaitOne method as
follows:
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 this token. 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
}
);
117
CHAPTER 5 MANAGING CANCELLATIONS
Demonstration 3
It is time for another demonstration where I show you the discussed
approaches to monitor the cancellation operation. Notice the key changes
in bold:
token.Register(
() =>
{
WriteLine("Cancelling the print activity.[Using event
subscription]");
// Do something else, if you want
}
);
118
CHAPTER 5 MANAGING CANCELLATIONS
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
}
);
119
CHAPTER 5 MANAGING CANCELLATIONS
Output
Here is one sample output. Notice the changes in bold.
120
CHAPTER 5 MANAGING CANCELLATIONS
Demonstration 4
In the following program, a user can trigger a normal cancellation.
However, you can also observe an unexpected/emergency cancellation.
To mimic an emergency cancellation, I rely on a random number generator.
If the random number is 5, the unexpected cancellation will be triggered.
Here is the complete program to demonstrate the idea:
tokenNormal.Register(
() =>
{
WriteLine("Processing a normal cancellation.");
121
CHAPTER 5 MANAGING CANCELLATIONS
tokenUnexpected.Register(
() =>
{
WriteLine("Processing an unexpected cancellation.");
// Do something else, if you want
}
);
122
CHAPTER 5 MANAGING CANCELLATIONS
Output
Here is a sample output when a user pressed “c” to initiate a normal
cancellation:
123
CHAPTER 5 MANAGING CANCELLATIONS
Summary
Cancellation is an essential mechanism in task programming. However,
instead of abruptly stopping a task, you make a cooperative model where
the task and the calling code (that initiates the cancellation) can work
together to maintain the health of your application. This chapter discussed
this topic and answered the following questions:
124
CHAPTER 5 MANAGING CANCELLATIONS
Exercises
Check your understanding by attempting the following exercises:
REMINDER
As said before, you can safely assume that all other necessary namespaces
are available for these code segments. The same comment applies to all
exercises in this book as well.
E5.1 If you execute the following code, can you predict the output?
125
CHAPTER 5 MANAGING CANCELLATIONS
}, token
);
Thread.Sleep(500);
WriteLine("The cancellation is 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.");
if (token.IsCancellationRequested)
{
WriteLine("Cancelling the print activity.");
return;
}
token.ThrowIfCancellationRequested();
WriteLine("Exercise 5.3");
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
126
CHAPTER 5 MANAGING CANCELLATIONS
() =>
{
Thread.Sleep(1000);
if (token.IsCancellationRequested)
{
WriteLine( "The cancellation request is raised too early.");
token.ThrowIfCancellationRequested();
}
WriteLine("The parent task is running.");
// Creating a nested task
child = Task.Factory.StartNew(
() =>
{
WriteLine("The child task has started.");
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
WriteLine($"\tThe nested task prints:{i} ");
Thread.Sleep(200);
}
return "The child task has finished too";
},
token,
TaskCreationOptions.AttachedToParent,
TaskScheduler.Default);
child.Wait(token);
},token);
WriteLine("Enter c to cancel the nested task.");
char ch = ReadKey().KeyChar;
127
CHAPTER 5 MANAGING CANCELLATIONS
if (ch.Equals('c'))
{
WriteLine("\nTask cancellation requested.");
tokenSource.Cancel();
}
try
{
parent.Wait();
}
catch (AggregateException ae)
{
foreach (Exception e in ae.InnerExceptions)
{
WriteLine($"Caught error: {e.Message}");
}
}
WriteLine($"The current state of the parent task: {parent.
Status}");
string childStatus = child != null ? child.Status.ToString() :
"not created";
WriteLine($"The current state of the child task: {childStatus}");
WriteLine("End of the main thread.");
128
CHAPTER 5 MANAGING CANCELLATIONS
Solutions to Exercises
Here is a sample solution set for the exercises in this chapter.
E5.1
The program will automatically initiate a cancellation. Here is a possible
output (notice that the task status is RanToCompletion but not Canceled):
E5.2
This time the final task status should appear as Canceled. Here is a
sample output:
E5.3
You already know that this program creates a parent task and a nested task.
It also lets you cancel the nested task if you press “c” quickly. As a result,
depending on the situation, you may see a different output. For example,
if you do not initiate a cancellation and press the Enter key at the end, you
can see the following output:
Exercise 5.3
Enter c to cancel the nested task.
129
CHAPTER 5 MANAGING CANCELLATIONS
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 can see the following:
Exercise 5.3
Enter c to cancel the nested task.
c
Task cancellation requested.
The cancellation request is raised too early.
Caught error: A task was canceled.
The current state of the parent task: Canceled
The current state of the child task: not created
End of the main thread.
130
CHAPTER 5 MANAGING CANCELLATIONS
Exercise 5.3
Enter c to cancel the nested task.
The parent task is running.
The child task has started.
The nested task prints:0
The nested task prints:1
c
Task cancellation requested.
Caught error: A task was canceled.
The current state of the parent task: Canceled
The current state of the child task: Canceled
End of the main thread.
E5.4
The answers are shown inline in bold:
i) The CancellationTokenSource is a class that
implements the IDisposable interface. [True]
ii) The Token property of the CancellationTokenSource
class is used to generate the CancellationToken
instance. [True]
E5.5
I leave this exercise to you now. Good luck!
Additional note: This time onward, while solving the exercises, you can
exercise applying cancellation and exception mechanisms. The same
comment applies to the exercises that you solved in the previous chapters.
131
CHAPTER 6
Bonus
This chapter discusses some additional topics on task programming.
Progress Reporting
You can see the progress status while updating the operating system or
installing a new version of Visual Studio on a computer. Let’s see whether you
can create an application with a similar feature using task programming.
Demonstration 1
However, while processing those records, if you do not show the progress
status, the user can be confused. To make things simple, let’s see a sample
demonstration that deals with only five records as follows:
Output
Here is a sample output that should not cause any surprise for you:
Analysis
However, you’ll see that the line “All the records are processed.” has
appeared after a significant amount of time. From a user perspective, it is
a confusing behavior because after you see the line “Starts processing
the records...”, you do not know what is happening in the background.
You understand that progress reporting can help you in a similar
context, particularly when you execute a long-running task.
Now, the question is: how can you report the progress? Well, it’s up to
you. For example, you may simply try to print which record is currently
134
Chapter 6 Bonus
135
Chapter 6 Bonus
This gives you a clue that you can pass a delegate as a parameter to
the constructor of the Progress class. This delegate effectively acts as an
event handler that you can use to report the progress. Let me update the
previous code to demonstrate how it works.
First, I changed the ProcessRecords function, so that it can accept
IProgress<int> as a parameter. Let us see the modified function with the
key changes in bold.
POINTS TO NOTE
136
Chapter 6 Bonus
Demonstration 2
Let us see the modified demonstration now:
137
Chapter 6 Bonus
Output
Here is a sample output:
You can see that the program can successfully print the update status.
138
Chapter 6 Bonus
method, called Invoke, that helps you create multiple tasks that can run
concurrently.
Author's note: Since the first part (i.e., the task parallelism constructs) is
already covered in this book, I’m not talking about it in this section.
Using Parallel.Invoke
The online link https://learn.microsoft.com/en-us/previous-
versions/msp-n-p/ff963549(v=pandp.10)?redirectedfrom=MSDN states:
Parallel.Invoke is the simplest expression of the parallel task
pattern. It creates new parallel tasks for each delegate method
that is in its params array argument list. The Invoke method
returns when all the tasks are finished.
At the time of this writing, there are two overloads for the parallel
Invoke method. Let me consider the simplest one that has the
following look:
You understand that while using this method, you can pass a variable
number of Action instances to the Invoke method. Let me create three
such instances and pass them in the following program.
Demonstration 3
Here is the complete demonstration.
#region Parallel.Invoke
WriteLine("Using Parallel.Invoke method.");
Action greet = new(() => WriteLine($"Task {Task.CurrentId}
says: Hello reader!"));
139
Chapter 6 Bonus
Output
Here is a sample output:
Additional Suggestions
Before I finish this section, I have the following suggestions for you:
• To have greater control over task executions, you’d like
to create and execute tasks explicitly.
140
Chapter 6 Bonus
Q&A Session
Q6.1 I can see that task 2 finishes after task 3 in the previous output. Is
this an expected behavior? I can also see that you did not wait for the
tasks to be finished. Is this OK?
Yes. The online link https://learn.microsoft.com/en-us/dotnet/
api/system.threading.tasks.parallel.invoke?view=net-9.0 states:
This method can be used to execute a set of operations, poten-
tially in parallel. No guarantees are made about the order in
which the operations execute or whether they execute in par-
allel. This method does not return until each of the provided
operations has completed, regardless of whether completion
occurs due to normal or exceptional termination.
Q6.2 It appears to me that I could create separate tasks and wait for
them to finish their executions instead of using Parallel.Invoke. Is this
correct?
Nice observation. The online link https://learn.microsoft.com/
en-us/previous-versions/msp-n-p/ff963549(v=pandp.10)?redirected
from=MSDN confirms that by saying the following:
Internally, Parallel.Invoke creates new tasks and waits for
them. It uses methods of the Task class to do this.
If you study the mentioned link, you will understand that I could write
an equivalent program as follows:
141
Chapter 6 Bonus
However, notice that when you work with a large number of delegates,
creating separate tasks for each of those delegates and managing them is
not a good idea. The use of Parallel.Invoke can give you the relief! It will
work efficiently in those cases as well.
Q6.3 How does the data parallelism differ from the task parallelism?
Microsoft (https://learn.microsoft.com/en-us/previous-
versions/msp-n-p/ff963549(v=pandp.10)?redirectedfrom=MSDN)
summarizes the difference nicely by saying the following:
Data parallelism and task parallelism are two ends of a
spectrum. Data parallelism occurs when a single operation
is applied to many inputs. Task parallelism uses multiple
operations, each with its own input.
Precomputed Tasks
One of the primary benefits of performing asynchronous operations
is faster executions. However, despite your best efforts, some of the
operations are indeed time-consuming. To tackle such a situation, you can
use the caching mechanism. Let’s explore it in more detail.
Without Caching
The following program exercises a time-consuming method, named
TimeConsumingMethod. Each time you call this method, you need to
wait more than three seconds to get a random number. Let us exercise a
program that invokes this method multiple times.
142
Chapter 6 Bonus
Demonstration 4
Here is a sample demonstration.
using System.Diagnostics;
using static System.Console;
stopwatch.Restart();
// Subsequent calls
WriteLine(Sample.TimeConsumingMethod().Result);
stopwatch.Stop();
WriteLine($"Elapsed time: {stopwatch.ElapsedMilliseconds}
milliseconds");
class Sample
{
static int flagValue = 0;
public static Task<int> TimeConsumingMethod()
{
return Task.Run(
() =>
{
WriteLine("Forming the value...");
// Simulating a delay before forming the value
Thread.Sleep(3000);
flagValue = new Random().Next(0, 100);
return flagValue;
}
143
Chapter 6 Bonus
);
}
}
Output
Here is a sample output:
Analysis
You can see that every time you invoke the time-consuming method, it
takes more than three seconds to return a number.
144
Chapter 6 Bonus
Demonstration 5
Let us modify the time-consuming method in the Sample class as follows:
145
Chapter 6 Bonus
Output
Here is a sample output:
Analysis
You can see that the caching mechanism improved response time
significantly in the subsequent calls.
Q&A Session
Q6.4 Can you give me a real-world example where I can use
precomputed tasks?
Suppose, there is an application where a user provides a URL. Then
the application starts downloading the data from that URL for further
processing. In this case, to avoid repeated downloads, you can store the
URL and the corresponding data in a cache.
Using TaskCompletionSource
You have seen that tasks can help you perform background work. They
are also useful for managing work items like performing continuation
works, managing child tasks, or handling exceptions. However, in
some cases, you may need to have more control over the tasks. The
TaskCompletionSource<TResult> class can be useful in those scenarios.
Let’s explore its usage.
146
Chapter 6 Bonus
How to Use?
Now the question is: how to 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. Here is a sample screenshot from Visual Studio that
shows the details of this class (see Figure 6-1).
147
Chapter 6 Bonus
From this screenshot, you can see that the method names start either
with “Set” or “TrySet”. The first category returns void, but the second
category returns bool. You can note the following points about these
methods:
148
Chapter 6 Bonus
Demonstration 6
Let’s see a demonstration. To understand the following program, read the
following points:
• At the beginning of the program, I created an instance
of TaskCompletionSource<string> class, called tcs. As
a result, I can use its Task property in a later stage.
149
Chapter 6 Bonus
Output
Here are some sample outputs.
Case 1 (the user opts for the detail):
Thank you!
Case 2 (the user does not opt for the detail and types “n”):
150
Chapter 6 Bonus
Q&A Session
Q6.5 Before Demonstration 6, you said the following: “You should call
the first category (i.e., where the method names start with the word
“Set”) exactly once; otherwise, you’ll see exceptions.” Can you please
elaborate?
If you use the following version of the backgroundTask :
You can see an identical output. Let’s try to set the result one more
time as follows:
Now, wait for this task to finish (e.g., using the line backgroundTask.
Wait(); in the client code); you will see the following exception in the
final output:
151
Chapter 6 Bonus
Summary
This chapter provides some supplementary material that can help you in
task programming. Upon completion of this chapter, you can answer the
following questions:
• How can you report progress while executing a long-
running task?
• How can you create tasks implicitly?
152
Chapter 6 Bonus
Exercises
Check your understanding by attempting the following exercises:
REMINDER
as said before, you can safely assume that all other necessary namespaces
are available for these code segments. the same comment applies to all
exercises in this book as well.
E6.1 If you execute the following code, can you predict the output?
153
Chapter 6 Bonus
E6.2 Can you predict the following output of the following program?
E6.3 Can you predict the following output of the following program?
Solutions to Exercises
Here is the solution set for the exercises in this chapter.
154
Chapter 6 Bonus
E6.1
You should see the following output:
[Clue: Notice that task1 updates the initial value to 11, but task2 further
sets it as 11*10=110. The Wait statement was placed to preserve the order
of evaluation.]
E6.2
You should see the following output:
E6.3
Here is the output:
Hello reader!
There is a problem.
Author's note: This output is predictable. Why? You’ll always see the error
message "There is a problem" after the line "Hello reader!". The
online link https://learn.microsoft.com/en-us/previous-versions/
msp-n-p/ff963549(v=pandp.10)?redirectedfrom=MSDN confirms that by
saying the following:
Any exceptions that occur during the execution of Parallel.
Invoke are deferred and rethrown when all tasks finish. All
exceptions are rethrown as inner exceptions of an
AggregateException instance.
155
APPENDIX A
What’s Next?
Task programming is the foundation of modern-day asynchronous
programming. I hope that after completing this book, you get a fair idea
about it. Now I suggest you read the related topics from other books,
articles, or blogs. Most importantly, you should keep experimenting with
new code and learn more. We all know that practice makes a man perfect.
The next step is exercising async and await keywords in your program.
A dedicated pocketbook in this series will cover that topic. You may note
that I wrote a book on parallel programming (Parallel Programming with
C# and .NET (Apress, 2024)) which covers that topic as well. If you like
this book, you may want to learn more about asynchronous and parallel
programming from that book.
I always keep coding, practicing programs, and learning from others.
This is why in the following recommended list, you will see a few more
books, courses, and articles from which I got many new insights. I believe
that they will be equally effective for you. You can learn more from these
materials (or their updated editions) as well.
Books
Here is my recommended list of books:
• C# 12 in a Nutshell by Joseph Albahari (O’Reilly Media,
first edition, December 2023)
Courses
The following list includes helpful online courses. These cover a wide
number of topics. At the time of this writing, none of them are free.
However, you may get a promotional discount occasionally on these
courses.
• https://www.udemy.com/course/parallel-dotnet/
learn/lecture/5645430#overview
• https://www.udemy.com/course/parallel-csharp/
learn/lecture/11126093#overview
Other Resources
In each chapter, you have seen various online resources in the discussions
and the “Q&A Sessions.” You can have a detailed look at those resources to
learn more about them.
158
APPENDIX B
Other Books
by the Author
The following list includes other Apress books by the author:
• Parallel Programming with C# and .NET (Apress, 2024)
To learn more about these books, you can refer to any of the
following links:
• https://amazon.com/author/vaskaran_sarcar
• https://link.springer.com/search?newsearch=true
&query=vaskaran+sarcar&content-type=book&dateFr
om=&dateTo=&sortBy=newestFirst
160
Index
A, B requests, 99
shortening the code, 108–110
AggregateException, 75
user-initiated, 102–106
demonstration, 76, 77
CancellationToken, 32, 100, 110,
Q&A session, 78, 79
128, 131
Asynchronous programming,
CancellationTokenSource, 100,
1, 24, 29
102, 110, 128, 131
description, 2
Collection expressions, 50
modern-day computers, 2
Conditional continuation tasks
patterns, 4, 5
analysis, 51
Q&A session, 3, 4
ContinueWhenAll method, 49
recommended pattern, 5
demonstration, 47, 48, 50
scenarios, 2, 3
output, 48, 51
AsyncResult pattern, 5
TaskContinuationOptions, 46–48
Attached nested task, 60–61
Continuation tasks, 41, 42
conditional continuations,
C 45–52
Caching mechanism, 144 simple continuation, 42–45
analysis, 146 ContinueWhenAll method, 49, 51
demonstration, 145 ContinueWhenAny method, 51, 52
Q&A session, 146 CPU-bound needs, 3
Callback methods, 4 CreateLinkedTokenSource
Cancellations of tasks, 33, 116, See method, 120, 121
also Task cancellation
OperationCanceledException
(CancellationToken), D
111, 112 Data parallelism, 138, 142
prerequisites, 100–102 Delay method, 28–30, 40
F, G, H
Fine-grained parallelism, 8 O
Forcing parent task, 61 Object-oriented programming
demonstration, 62, 63 (OOP), 1, 79
Functional programming OperationCanceledException
(FP), 1, 79 (CancellationToken),
111, 112
orderTask, 43, 49
I, J, K
InsufficientMemory
Exception, 85, 86 P, Q
InvokeTasks method, Parallel.Invoke, 139
86, 88 demonstration, 139
I/O bound needs, 3 output, 140
IProgress, 135 Parallel programming, 1, 3, 4, 157
162
INDEX
163
INDEX
164