Understanding Tasks - Parallel Programming With C# and .NET - Fundamentals of Concurrency and Asynchrony Behind Fast-Paced Applications
Understanding Tasks - Parallel Programming With C# and .NET - Fundamentals of Concurrency and Asynchrony Behind Fast-Paced Applications
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2024
V. Sarcar, Parallel Programming with C# and .NET
https://doi.org/10.1007/979-8-8688-0488-5_1
1. Understanding Tasks
Vaskaran Sarcar1
(1) Near Garia Station, Post: Garia, Kuntala Furniture, 2nd Floor, Kolkata, West
Bengal, India
Modern-day life is full of work. Consider those working mothers who
need to complete a lot of housework before they go to the office. You may
see them performing all these activities asynchronously. For example,
they start preparing breakfast, and they come back from the kitchen to
prepare the kids for school. Then they go back to the kitchen to check the
status of the food only to leave the kitchen to start preparing themselves
for the office. You can see that with this approach, when they start a task,
they do not wait for that task to complete; instead, they start the next task
and then go back to check the status of the previous task. This process
continues until all these tasks are finished. Psychologists who vote for do-
ing one task at a time may not like this approach, but at the same time, we
cannot ignore that this is how people do things. The programming world
tries to mimic real-world scenarios. Throughout this book, we will ex-
plore those possibilities using C# and .NET.
You are probably familiar with synchronous method calls where you see that the
caller method is blocked until the callee method finishes its execution. To
illustrate, consider the following code segment:
However, it is not a good idea to always rely on the blocking calls. For ex-
ample, consider the situation when you trigger a method that attempts to
download a large file. While the download is in progress, if you cannot do
any other work, that is a problem for sure. What is the solution? You
guessed it: you will opt for a multithreading environment and opt for
asynchronous operations.
You may also notice that computing machines are becoming faster by the
day. The role of multicore CPUs is inevitable. So, regardless of the pro-
gramming languages, developers will want to take advantage of them.
This is why parallel programming is becoming an essential technique for
developing powerful software that is highly responsive.
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 1/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Parallel programming is interesting as well as challenging. Undoubtedly it is
hard, but in the past it was harder. In those days, developers had the following
options:
These are not the recommended approaches now. I’d like to start the
discussion with tasks that make up the foundation of the Task Parallel Library
(TPL). Why? Once .NET Framework 4 was released, Microsoft stated the following
(see https://learn.microsoft.com/en-
us/dotnet/standard/parallel-programming/task-based-
asynchronous-programming):
TPL is the preferred API for writing multithreaded, asynchronous,
and parallel code in .NET
NoteIt is also interesting to know that behind the scenes, tasks are
queued to the ThreadPool class, which determines and adjusts the num-
ber of threads. The ThreadPool class follows some algorithms to control
the load balancing to gain maximum throughput.
System.Threading
System.Threading.Tasks
Author’s Note: Developers often refer to the Parallel class with the task
parallelism constructs as the TPL. You’ll learn about the Parallel class in
Chapter 4. However, at this stage, you do not need to worry about them.
You’ll learn about these constructs one at a time.
You can easily guess that we are going to cover all these scenarios. For
now, it will be sufficient for you to understand that the TPL simplifies the
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 2/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
A task is a code block that represents a unit of work. You use tasks to inform
the scheduler that this code block can execute on a separate thread while the
main thread of execution continues. For example, consider the following code
segment:
This can be a unit of work, and we can execute it on a separate thread. Some
common examples of tasks include the following:
You can create and execute tasks in different ways. Let me show you a sample
code segment that creates and starts executing the task:
From C# 9 onward, you can use target-typed new expressions. So, the
previous code can be further simplified as follows:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 3/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
WriteLine("Starts executing the task1.");
// Simulating a delay
Thread.Sleep(1);
WriteLine("The task1 is finished.");
}
);
task1.Start();
NoteI have introduced the delay using the Thread.Sleep method. Later,
you’ll also see me using Task.Delay in a similar context, particularly
when I discuss asynchronous programming in more depth in Chapter 6.
The Alternative Approaches
I told you that you can create and execute tasks in different ways. Here I
present you with two more approaches.
Approach 2: You can create and execute a task in a single operation using
Task.Run method as follows:
Alternative 3: You can also create and execute tasks in a single operation
using TaskFactory.StartNew method. The following code segment
demonstrates the usage:
Demonstration 1
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 4/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
The following program demonstrates the creation and execution of three
different tasks on three different threads. Here are two notable characteristics:
You’ll see three different approaches for task creation and execution
that I just discussed.
In addition, this example also includes the following lines to ensure
that these tasks finish their job before the console mode application
ends.
task1.Wait();
task2.Wait();
task3.Wait();
POINT TO REMEMBER
From .NET 6 onwards, you may notice the presence of implicit global di-
rectives for new C# projects. This helps you use the types in these name-
spaces without specifying the fully qualified names or manually adding a
“using directive.” You can learn more about this at
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:
System
System.Collections.Generic
System.IO
System.Linq
System.Net.Http
System.Threading
System.Threading.Tasks
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 5/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
{
Write($"Task-2 prints {i}\t");
// Simulating a delay
Thread.Sleep(1);
}
WriteLine("Task-2 is completed.");
}
);
// Approach-3
Task task3 = Task.Factory.StartNew(
() =>
{
WriteLine("Task-3 starts.");
for (int i = 320; i < 325; i++)
{
Write($"Task-3 prints {i}\t");
// Simulating a delay
Thread.Sleep(1);
}
WriteLine("Task-3 is completed.");
}
);
WriteLine($"The main thread is doing some other work...");
Thread.Sleep(10);
WriteLine($"Main thread is completed.");
task1.Wait();
task2.Wait();
task3.Wait();
Output
Here is some sample output (the output may vary on your system). You can see
that there is a nice mixture of output from all the different threads/tasks.
Q&A Session
The tasks are relatively lightweight. They will help you achieve fine-
grained parallelism.
Later you’ll see that by using the built-in API for tasks, you can easily
exercise useful operations such as waiting, cancellations, continua-
tions, custom scheduling, or robust exception handling. So, when you
opt for tasks instead of threads, you’ll have more programmatic
control.
Q1.2 To create and execute tasks, you have shown me the use of the
Run, Start, and StartNew methods. How can I decide which one is best
for me?
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 6/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
If you see the method definitions in Visual Studio, you will see the
following.
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.
Microsoft further says that you can use the StartNew() method for the
following situations:
POINT TO NOTE
Shortly, you’ll learn about child tasks (or nested tasks). You’ll learn that by 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. Here is such an overload:
But, in case you use the Run method, a similar option is not available for
you.
In this section, I’ll discuss how you can pass values to a task or get back a
computed value from a task.
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 7/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Passing Values into Tasks
Let’s start the discussion on passing values into tasks. Consider the following
code:
If you want to execute this method on a separate thread, you need to pass a
valid argument for the limit parameter from the calling thread. How can you do
that? You can use the following lines of codes:
() => PrintNumbers(10)
What does this indicate? This lambda does not take any argument, but I
pass the required argument explicitly.
Let’s investigate an alternative approach. At the time of this writing, the Task
class has the constructors in Figure 1-1.
In the same way, you can investigate the StartNew method too. At the time of
this writing, the StartNew method has 16 overloads in the TaskFactory class,
and one of them is as follows:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 8/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
// Remaining code not shown
}
The previous two code segments give 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:
Or the following:
You have now seen five different approaches while passing a state so far. Let’s
summarize them:
// Approach-1:
var task1 = new Task(() => PrintNumbers(10));
task1.Start();
// Approach-2:
var task2=Task.Factory.StartNew(() => PrintNumbers(10));
// Approach-3:
var task3 = Task.Run(() => PrintNumbers(10));
// 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. But, notice that in the
last two cases, the target method (PrintNumbersVersion2) expected an
object. As a result, these two approaches suffer from the impact of box-
ing and unboxing. On the contrary, they look cleaner compared to ap-
proaches 1, 2, or 3. In the end, it is up to you how you want to organize it.
When you execute a task, you may need to access the final value. In such cases,
you need to use the generic version of the Task class and the Result property.
Here is a sample:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 9/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
However, Visual Studio will help you recognize that the type argument can be
inferred, and as a result, you can further simplify this code as follows:
Let’s see a complete program where you deal with some tasks, pass some
values into them, and finally retrieve the computed result.
Demonstration 2
In the following demonstration, I create two tasks where the first task cal-
culates the factorial of an integer (5) and the second task adds two inte-
gers (25 and 17). Once these tasks are finished, I retrieve the computed re-
sults and display them in the console window. Let’s see the complete pro-
gram now.
Output
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 10/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Here is the output:
Q&A Session
Q1.3 I can see that in the previous demonstration, you did not use the
lines task1.Wait(); and task2.Wait();. Was this intentional?
When you want to get a result from a task, you need to wait until the task
finishes its execution. It means you need to invoke a blocking operation.
Using the Result property, I did the same: I blocked the calling thread un-
til those tasks were finished. As a result, I did not need to use the line
task1.Wait() or task2.Wait() separately.
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, you’d probably like to use callbacks.
But the TPL makes it easy. It provides the functionality through a continu-
ation task, which is just an asynchronous task. The idea is the same: once
an antecedent task finishes, it invokes the next task that you want to con-
tinue. In our example, Task-A is the antecedent task, and Task-B is the
continuation task.
You understand that continuation is just chaining tasks. This is helpful when
you want to pass data (or, execute some logic) from an antecedent to the
continuation task. The TPL provides many built-in supports in this context.
Simple Continuation
Assume that a person wants to invite his friends for dinner. At a high level, let’s
divide the overall activity into three different tasks as follows:
1.Order food.
2.Invite friends.
3.Arrange dinner.
Let’s start with a simple continuation scenario where the host decides to do
these steps in order, with ordering food at the beginning. Then he invites his
friends, and finally, he arranges the dinner. Since the process starts with ordering
food, you will see the following task at the beginning:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 11/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
var task1 =
Task.Factory.StartNew(() => WriteLine("Ordering food."));
Since the task of invitation comes after ordering food, this time you’ll notice
the use of the ContinueWith method. This method creates a continuation that
executes when the target task is completed. At the time of this writing, there are
20 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:
var task2 =
task1.ContinueWith((t) => WriteLine("Inviting friends."));
Similarly, since arranging dinner comes after the second task (task2) finishes,
you will see the following code:
var task3=
task2.ContinueWith((t) => WriteLine("Arranging dinner."));
Finally, to show you the output messages, I’ll wait for the final task (task3) to
finish. This is why you will notice the following line at the end of the program:
task3.Wait();
Demonstration 3
Output
Ordering food.
Inviting friends.
Arranging dinner.
Specialized Continuation
Case Study 1
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 12/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Let’s assume you want to confirm that task3 executes when both task1 and
task2 complete their executions. In this case, you’ll see me using the
ContinueWhenAll method. As usual, there are many overloads of this method. I
am about to use the following one that accepts two parameters. This method has
the following form:
This is why you’ll see the following line that indicates that task1 and task2
must be completed before you start a continuation task:
Demonstration 4
Output
Ordering food.
Inviting friends.
Arranging dinner.
Here is some other possible output where the invitation is done at the
beginning:
Inviting friends.
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 13/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Ordering food.
Arranging dinner.
Analysis
In every case, you can see that dinner has been arranged only after the
task of ordering food is completed and the invitations are done.
POINT TO REMEMBER
You may note that the “Collection expressions” feature in C# 12 allows us to
rewrite task3 as follows (notice the change in bold for your reference):
Q&A Session
Q1.4 I can see that you did not use the lambda parameter tasks in the
following code:
tasks =>
{
WriteLine("Arranging dinner.");
}
This indicates that you could omit the parameter. Is this correct?
Nice observation. I was forced to use this parameter to get support from the
built-in construct. If you do not use the parameter in this place, you’ll see the
following error:
But this parameter is useful when you want to analyze the individual tasks
that were finished before. For example, see the following program:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 14/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
#endregion
Ordering food.
Inviting friends.
Arranging dinner.
Author’s Note: The C# compiler can also infer the type Task<string>, as
shown in the commented code. From C# 12 onward, you can further sim-
plify the code, which is also shown in the commented line.
Case Study 2
Let’s analyze one more case study where you continue a task (say, task3)
if any one of the previous tasks (say task1 or task2) completes the execu-
tion. In this case, you can use ContinueWhenAny (instead of
ContinueWhenAll). For example, you can replace ContinueWhenAll with
ContinueWhenAny in the previous code, and you will get any of the follow-
ing outputs:
Possible Output 1:
Ordering food.
Arranging dinner.
Possible Output 2:
Inviting friends.
Arranging dinner.
Possible Output 3:
Ordering food.
Inviting friends.
Arranging dinner.
Possible Output 4:
Inviting friends.
Ordering food.
Arranging dinner.
Nowadays computers are very fast. As a result, you may not see the possi-
ble case 1 or possible case 2. So, to demonstrate the working mechanism
of ContinueWhenAny, let me simulate some delay inside task1 and task2.
I assume ordering food can be faster than inviting friends. So, I introduce
Thread.Sleep(10) inside task1 and Thread.Sleep(100) inside task2.
Demonstration 5
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 15/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Output
This time the probability of getting the following output is very high:
Ordering food.
Arranging dinner.
Author’s Note: I hope that you now understand the concept of task con-
tinuations. I suggest you exercise a program where you check the result
of an antecedent task before you continue a new task. I leave this as an
exercise for you.
Nested Tasks
The following code creates two Task instances, called parent and child. Notice
that the child task is created inside the parent task.
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 16/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Here is some possible output once you run this program:
Notice that this output does not reflect whether the child task is finished.
Why? In the following documentation, Microsoft provides the following
information (see https://learn.microsoft.com/en-
us/dotnet/standard/parallel-programming/attached-and-
detached-child-tasks):
A child task (or nested task) is a System.Threading.Tasks.Task in-
stance that is created in the user delegate of another task, which is
known as the parent task. A child task can be either detached or at-
tached. A detached child task is a task that executes independently of
its parent. An attached child task is a nested task that is created with
the TaskCreationOptions.AttachedToParent option whose parent
does not explicitly or by default prohibit it from being attached. A
task may create any number of attached and detached child tasks,
limited only by system resources.
Using TaskCreationOptions
Demonstration 6
Output
Run this program and notice the output. You can see that this time the child task
completed its execution too:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 17/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Q&A Session
parent.Wait();
This means you are not waiting for the child task to finish. As a re-
sult, there is no guarantee that the output will reflect whether the
child task has finished. Is this a correct understanding?
No. Microsoft has designed the architecture in such a way that if you cre-
ate a parent-child relationship, waiting on the parent task forces you to
wait for the child task to complete.
Q1.6 At the beginning of this section, when the child task was not at-
tached to the parent, I saw that the output did not reflect whether the
child task completed its execution. You explained the scenario saying
that in that code segment, you waited only for the parent task but not
for the child task. So, if I replace the line parent.Wait(); with
Task.WaitAll(parent, child);, I can see whether the child task fin-
ishes 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
Using TaskContinuationOptions
You have already seen a demonstration where a parent task waits for a child task
to finish. Also, I have printed a message in the console saying that the child task
has finished. But there is a better way to check whether the task was completed
properly. For example, if you want to verify the status of the child task, you can
introduce another task that will continue after the child task as follows:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 18/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Demonstration 7
NoteYou can see that I have used Task.CurrentId to get the ID of the task
that is currently executing. You can use the same for debugging purposes
in a parallel environment.
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 19/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
WriteLine($"Task id {task.Id}'s status is
{task.Status}.");
},
TaskContinuationOptions.AttachedToParent);
WriteLine("The parent task has finished now.");
}
);
parent.Wait();
Output
Analysis
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
TaskStatus is an enum type and has many members. Figure 1-4 the members in
Visual Studio.
RanToCompletion
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 ex-
ception. I assume that I need not mention that Canceled indicates that a
task was canceled, which can occur due to various reasons such as user
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 20/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Now you understand that in the previous output, the task (which had ID
10) was completed successfully.
Q&A Session
Q1.7 In the previous output, I see the IDs of the parent task and the
child task are 9 and 10, respectively. It looks like many other tasks
were also running. Is this correct?
Yes, I ran this code in Visual Studio 2022 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 following output:
POINT TO REMEMBER
I often execute my programs in debug mode. So, to get 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.
Q1.8 In your example, the child task was completed successfully. But
you told me that the final state of a task can be different. For exam-
ple, a child task can encounter an exception or someone can cancel it
while it is running. How can you handle these scenarios?
You can use separate branches to handle different scenarios. Since I have not
discussed task cancellation and exception handling yet, I showed you an example
that could be completed easily. Once you learn about handling those special
scenarios in Chapter 2, the following program will be easy for you to understand.
For now, you can concentrate on the following code that demonstrates two
possible cases where a child task can finish successfully if a user does not press C
immediately (more specifically, within five seconds once the child task starts).
Otherwise, it will be canceled. This is why you’ll see the following two branches:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 21/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
},
TaskContinuationOptions.AttachedToParent |
TaskContinuationOptions.OnlyOnCanceled);
Demonstration 8
I have included this program to answer your question only. Since I have
not discussed cancellations yet, you may not understand all the lines in
this program. For now, you can focus on the two branches where one
branch handles the successful completion of a child task and the other
branch handles the cancellation of the child task. Still, if you find it diffi-
cult to understand, you can come back here after you learn how to cancel
a task (discussed in Chapter 2).
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 22/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Output
Here is the sample output when a user does not cancel the task:
Here is another sample output when the user raised a request to cancel the
child task:
Discussion of Waiting
When you execute a task and analyze the outcome, you must wait for the
task to finish. Now the question is: how can you wait? There are different
built-in constructs for this, and by using any of them, you can design your
application. In this chapter, you have already seen me using some of
those constructs.
In this section, I will show you the need to “wait,” and I will discuss some
useful methods to implement this idea.
NoteWaiting for a task literally blocks the current thread. So, in a parallel
execution environment, you must be careful before implementing the
idea.
Why Do We Wait?
First, let’s understand why you need a waiting mechanism. The following
demonstration will give you an idea.
Demonstration 9
Here is a program that runs two simple tasks. Let’s execute the following
program and analyze some of the possible outputs:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 23/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
WriteLine("Task2 ends.");
}
);
WriteLine("The end of main.");
Output
Output 1:
Output 2:
Analysis
The main thread ends before task1 and task2 finish their executions.
None of these outputs reflects whether task1 or task2 completes their
job.
How Do We Wait?
To see the final status of these tasks, you may need to wait. How can you
do that? There are different approaches. Let me show you some of them
in the following section.
Using Sleep
One of the simplest solutions is to block the main thread until the other tasks are
finished. Here is a sample where I block the main thread for 1,000 milliseconds:
This one line of additional code increases the probability of seeing some
output that reflects that task1 and task2 complete their executions before the
control leaves the main thread. Here is some possible output:
The advantage of using this approach is obvious. We can see that when
the main thread sleeps, the other tasks could execute their jobs. This indi-
cates that during sleep, the scheduler can schedule other tasks.
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 24/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
On the contrary, this approach has an obvious problem: you may block
the thread unnecessarily for some additional time. For example, I can see
the same output on my computer if I block the main thread for 500 mil-
liseconds or less. However, the problem is that since we cannot pre-
dict the exact time for these tasks to be completed, I need to block it
for a reasonable amount of time. Still, if any of these tasks take more
time to complete due to some other factors, you may not see any of the
task’s completion messages in the final output. This is a problem for sure!
Using Delay
If you replace the Thread.Sleep methods with Task.Delay methods, you may
see similar output. For example, in the previous code, let me replace the
statement Thread.Sleep(1000); with Task.Delay(1000); and run the
program again. Again, my computer shows different possible outputs, and one of
them is as follows:
This output reflects that task1 and task2 completed their jobs. However,
notice that the calling thread was not blocked this time. So, you see the
line The end of main. before the line Task2 starts.
In fact, Visual Studio says that when you use the Delay method, the call is not
awaited and the calling thread can continue before the call is completed (see the
message shown in Figure 1-5).
Figure 1-5 Visual Studio confirms that the call is not awaited
This gives you a clue that you should use Sleep for the synchronous
pauses whereas you should prefer the Delay method for nonblocking de-
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 25/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
lays. In this book, you’ll also see me using the Delay method in many
demonstrations (particularly, in Chapter 6 when I’ll use the async and
await keywords in my programs). The reason is that Task.Delay is more
suitable in asynchronous programming. While answering the question
Q1.12 in the Q&A Session, I’ll compare the Sleep method and the Delay
method in more detail.
Using ReadKey()
Using Wait
Earlier you saw that by using the Result property, I blocked the calling
thread until the specified tasks were finished. This means I was waiting
for those tasks to be completed. It is not necessary in every scenario to
analyze the outcome of task execution. In fact, a task may not return a
value at all. So, let’s search for alternatives for the waiting techniques.
First, let me remind you about the Wait method that you have seen already.
When invoking the Wait method on a Task instance, you can wait for it to
complete. Here is a sample where I call Wait on task1 and task2 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:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 26/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
WriteLine("The end of main.");
This change can also produce an output that reflects both tasks finished
their execution.
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:
To show you the effect of this change, let me introduce a small delay in task2
as follows:
Run the program now. From these outputs, you will see that the main
thread completes when at least one of these tasks finishes its execution.
Here I have included some samples that show by the time the main
thread finished, task1 completed its execution, but task2 could not do
that.
Output-1:
Output-2:
POINTS TO NOTE
Remember the following points:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 27/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Using WhenAny
Notice the previous outputs once again. You can see that the line The end
of main. came after task1 finished its execution. If you execute the pro-
gram repeatedly, you’ll never see that the mentioned line appears before
any of these tasks finish the execution. That is because, in the case of
WaitAny, the calling thread is blocked until any of those tasks finishes
the execution. Interestingly, there is another method, called WhenAny,
that does not block the calling thread.
Here is some sample output where you can see that the main thread
completes before task1 and task2:
To make the concept clearer, let me apply some modifications to the existing
program as follows:
Run this modified program now. The following output confirms that the use of
WhenAny does not block the calling thread. This is why I include a possible output
to show you that the line The end of main. appeared before task1 or task2
finished their executions. Here is some sample output:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 28/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
There will be situations when you need to be prepared for possible can-
cellations of tasks. In those cases, you need to have a cancellation token.
Since the topic is important as well as big, I will discuss it in the next
chapter.
Q&A Session
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 documentation
at https://learn.microsoft.com/en-
us/dotnet/api/system.threading.thread.spinwait?view=net-7.0
states the following:
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 comput-
ers with multiple processors. For example, on computers with multi-
ple Intel processors employing Hyper-Threading technology,
SpinWait prevents processor starvation in certain situations.
SpinUntil(Func<Boolean>)
SpinUntil(Func<Boolean>, Int32)
SpinUntil(Func<Boolean>, TimeSpan)
Let me show you how to use the simplest version that spins until the specified
condition is fulfilled. Here is an example where I wait until task1 completes its
execution properly:
Here is some possible output after this change is made to this program:
Note that this time the output shows that task1 completes the execution,
but it does not reflect whether task2 completes the execution. So, the key
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 29/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
takeaway is that there are different ways of waiting. You can use the one
that is more convenient for you.
POINT TO NOTE
I am mentioning only those methods that will be sufficient for you to un-
derstand the rest of this book. As said before, these methods have vari-
ous overloaded versions as well.
You can follow any of the traditional approaches. For example, you can use Visual
Studio’s intelligence or read Microsoft’s documentation to learn about them. For
example, Figure 1-6 shows that the Sleep method has two overloaded versions
and the first one accepts an int parameter.
Suppose you are working with two different tasks and each task works
with different URLs. Let’s further assume that each URL can help you test
the current health of a website. You understand that any of these links
will be sufficient to check the current status of a website. So, your pro-
gram can execute the tasks and continue as soon as you get the data. In
such a case, you can use WhenAny or WaitAny.
Unless there are sufficient reasons, I’ll opt for WhenAny in such a case, be-
cause it is nonblocking. This is because if a task waits to get a notification
from a set of tasks where none can be completed due to some unpre-
dictable circumstances, the use of WaitAny can cause a deadlock as well.
The Sleep method suspends the calling thread, which may not be ideal in
every scenario. The problem was already discussed, so I won’t discuss it
here. However, for a simple demonstration, this method is handy; so, I
used it in other demonstrations as well. Once you complete this book, I as-
sume that you’ll have a fair idea of how asynchronous programming
works in a parallel execution environment. In most cases, you’ll want to
use the Delay method. The obvious reason for this is that invoking the
Delay method does not cause the calling thread to be blocked. This is why
the use of the Delay method can help you build a more responsive UI.
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 30/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
While using the Delay method, you can also assign it to a task and await at a
later point in time as follows:
You should also note that the Delay method has various overloads, and
many of them accept CancellationToken as a parameter (this is dis-
cussed in the next chapter). Using this parameter, you can avoid aborting
the thread and terminate it nicely.
Exercises
POINT TO REMEMBER
As mentioned, for all code examples, the “Implicit Global Usings” was enabled in
Visual Studio. This is why you won’t see me mentioning the following
namespaces that are available by default:
System
System.Collections.Generic
System.IO
System.Linq
System.Net.Http
System.Threading
System.Threading.Tasks
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 31/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
);
printHelloTask.Wait();
WriteLine("End.");
E1.5 Starting with C# 12, we can define a primary constructor as a part of the
class declaration. Here is an example:
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 steps: first, it will verify whether the first task completes the
process successfully. In addition, it will print the current date and time.
Can you write a program fulfilling the criteria? (You do not need to han-
dle exceptions or cancellations for this exercise.)
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 32/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
Task.Delay(1000);
WriteLine("How are you?");
}, TaskCreationOptions.AttachedToParent);
});
helloTask.Wait();
Summary
This chapter gave you a quick overview of task programming. The discus-
sion started by introducing you to the TPL and described various aspects
of task creation and execution. It also described different approaches to
implementing a waiting mechanism for task completion.
Solutions to Exercises
E1.1
End.
Notice that you have created the task but you have not started this task.
E1.2
The program can show more than one possible output. You may think that it will
print the following:
Hello!
End.
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 33/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
But, notice that you did not wait for the task to finish its execution. So, you
may see End. before you see Hello.
End.
Hello!
It is also possible that in the output, you see End. only if the task takes some
extra time to start. To examine this, you can run the following code in which by
introducing some delay inside the task, I increase the probability of finishing the
main thread early:
E1.3
Hello!
End.
E1.4
30
End.
[Clue: 1*2+2*2+3*2+4*2+5*2=30]
E1.5
Here is a sample program based on the features that you learned in this chapter:
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 34/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
);
createEmp.Wait();
class Employee( string name,int id)
{
private string _name=name;
private int _id = id;
public override string ToString()
{
return $"Name: {_name} Id: {_id}";
}
}
E1.6
using System.Net;
using System.Net.NetworkInformation;
using static System.Console;
List<string> websites = ["www.google.com", "www.yahoo.com"];
// Same as:
// List<string> websites = new() {
// "www.google.com", "www.yahoo.com" };
var task1= Task.Run(()=>PrintHostNameAndIPs(websites));
var task2 = Task.Run(() => PingAll(websites));
Task.WaitAll(task1, task2);
void PrintHostNameAndIPs(List<string> urls)
{
urls.ForEach(url =>
{
IPAddress[] ips = Dns.GetHostAddresses(url);
WriteLine($"{url}'s IP addresses are:");
ips.ToList().ForEach(WriteLine);
WriteLine("--------------");
});
}
void PingAll(List<string> urls)
{
urls
.Select(PingSite)
.ToList()
.ForEach(url => WriteLine($"{url.Address} ping
status: {url.Status} "));
}
static PingReply PingSite(string url)
{
return new Ping().Send(url);
}
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 35/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
2406:8600:f040:1fa::2000
27.123.43.204
27.123.43.205
--------------
2404:6800:4009:826::2004 ping status: Success
2406:8600:f040:1fa::3000 ping status: Success
POINT TO REMEMBER
I have used the “Collection expressions” feature that was introduced in C# 12.
This feature allows you to create a list as follows:
Additional Note
The shown program looks more functional and less object-oriented. If you are
biased toward OOP, you can consider the following alternative solution with the
key changes in bold:
using System.Net;
using System.Net.NetworkInformation;
using static System.Console;
List<string> websites = ["www.google.com", "www.yahoo.com"];
var task1 = Task.Run(() => PrintHostNameAndIPsOOP(websites));
var task2 = Task.Run(() => PingAllOOP(websites));
Task.WaitAll(task1, task2);
void PrintHostNameAndIPsOOP(List<string> urls)
{
foreach (string url in urls)
{
IPAddress[] ips = Dns.GetHostAddresses(url);
WriteLine($"{url}'s IP addresses are:");
foreach (IPAddress ip in ips)
{
WriteLine(ip);
}
WriteLine("--------------");
}
}
void PingAllOOP(List<string> urls)
{
List<PingReply> pingReplies = new();
foreach (string url in urls)
{
pingReplies.Add(PingSite(url));
}
foreach (PingReply reply in pingReplies)
{
WriteLine($"{reply.Address} ping status:
{reply.Status}");
}
}
static PingReply PingSite(string url)
{
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 36/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
return new Ping().Send(url);
}
E1.7
Hello reader!
Hello reader!
How are you?
You may wonder about this. Notice that I have used the Run method, but not
the StartNew method. Implement the following change (shown in bold):
Hello reader!
How are you?
In this context, I also suggest you see the online documentation that states
the following (see https://learn.microsoft.com/en-
us/dotnet/api/system.threading.tasks.taskfactory.startnew?
view=net-8.0):
Starting with the .NET Framework 4.5, you can use the
Task.Run(Action) method as a quick way to call StartNew(Action)
with default parameters. Note, however, that there is a difference in
behavior between the two methods regarding : Task.Run(Action) by
default does not allow child tasks started with the
TaskCreationOptions.AttachedToParent option to attach to the cur-
rent Task instance, whereas StartNew(Action) does.
E1.8
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 37/38
2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .NET: Fundamentals of Concurrency and Asynchrony Behind …
[Clue: In the case of an attached child task, the statement is true; but
for a detached child task, it is false.]
iv)Waiting for a task completion blocks the calling thread. [True]
v) The WaitAny method blocks the calling thread, but the WhenAny
https://learning.oreilly.com/library/view/parallel-programming-with/9798868804885/html/617342_1_En_1_Chapter.xhtml 38/38