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

Understanding Tasks - Parallel Programming With C# and .NET - Fundamentals of Concurrency and Asynchrony Behind Fast-Paced Applications

Uploaded by

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

Understanding Tasks - Parallel Programming With C# and .NET - Fundamentals of Concurrency and Asynchrony Behind Fast-Paced Applications

Uploaded by

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

2024/10/31 晚上9:05 1. Understanding Tasks | Parallel Programming with C# and .

NET: Fundamentals of Concurrency and Asynchrony Behind …

© 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.

Stepping into Parallel Development

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:

// Some code before


int temp = RetrieveSomeValue(); // Line 1
int result = Process(temp);// Line 2
// Some code after

If you exercise this code in a single-threaded environment, you cannot ex-


ecute line 2 before you get the temp value in line 1. Why? Calling the
RetrieveSomeValue method blocks the thread until the method finishes
its execution. We are probably all familiar with this type of coding. In
fact, we often exercise typical .NET calls that are blocking.

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:

Creating and using threads directly


Using the ThreadPool class
Using event-based asynchrony
Using the AsyncResult pattern
Using the callback methods

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.

Introduction to the Task Parallel Library

Why is the TPL important? As mentioned, handling concurrent programs and


adding parallelism to an application are challenging tasks. Undoubtedly, these are
advanced concepts of programming. The TPL aims to make them easy for you by
providing a set of public types and APIs. You can find them in the following
namespaces:

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.

How Does the TPL Help?

Microsoft’s documentation states the following (see


https://learn.microsoft.com/en-us/dotnet/standard/parallel-
programming/task-parallel-library-tpl):
The purpose of the TPL is to make developers more productive by
simplifying the process of adding parallelism and concurrency to
applications.

These are some of the useful scenarios for the TPL:

Managing a multithreaded environment efficiently


Scaling the concurrency level
Providing support for task cancellations
Managing state
Partitioning your work

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 …

multithreading scenarios and helps you write high-performance code


without worrying about the nitty-gritty of threading or other low-level
details.

The Concept of Tasks

What are tasks? Microsoft states the following (see


https://learn.microsoft.com/en-us/dotnet/standard/parallel-
programming/task-based-asynchronous-programming):
The Task Parallel Library (TPL) is based on the concept of a task,
which represents an asynchronous operation. In some ways, a task
resembles a thread or ThreadPool work item but at a higher level of
abstraction. The term task parallelism refers to one or more indepen-
dent tasks running concurrently.

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:

static void PrintNumbers()


{
WriteLine("Starts executing the task1.");
// Doing Some work
Thread.Sleep(5);
WriteLine("The task1 is finished.");
}

This can be a unit of work, and we can execute it on a separate thread. Some
common examples of tasks include the following:

Perform a calculation and display the result.


Compute a value with/without a supplied input.
Ask for a network resource.
Check the health of a website, for example, pinging a website.

Creating and Executing a Task

You can create and execute tasks in different ways. Let me show you a sample
code segment that creates and starts executing the task:

Task task1=new Task(PrintNumbers);


task1.Start();

From C# 9 onward, you can use target-typed new expressions. So, the
previous code can be further simplified as follows:

Task task1=new (PrintNumbers);


task1.Start();

Basically, you create a task by providing a delegate that encapsulates the


intended code. This delegate can be expressed as a named delegate, an
anonymous method, or a lambda expression. This is why I present in the
following task example, I encapsulate the necessary code inside a lambda
expression:

Task task1 = new(


() =>
{

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.");
}
);

Now I can start this task as follows:

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:

Task task2 = Task.Run(


() =>
{
WriteLine("Starts executing the task2.");
// Simulating a delay
Thread.Sleep(1);
WriteLine("The task2 is completed.");
}
);

Alternative 3: You can also create and execute tasks in a single operation
using TaskFactory.StartNew method. The following code segment
demonstrates the usage:

Task task3 =Task.Factory.StartNew(


() =>
{
WriteLine("Starts executing the task3.");
// Simulating a delay
Thread.Sleep(1);
WriteLine("The task3 is completed.");
}
);

You can choose the approach that suits you best.

NoteThe previous approaches that explicitly creates and execute tasks.


There are other approaches as well. For example, you can implicitly cre-
ate 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. The code examples and exercises in this book use
all these methods to create and execute tasks to make you familiar with
them.

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();

Let’s see the complete program now.

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

using static System.Console;


WriteLine("The main thread starts executing.");
// Approach-1
Task task1 = new(
() =>
{
WriteLine("Task-1 starts.");
for (int i = 100; i < 105; i++)
{
Write($"Task-1 prints {i}\t");
// Simulating a delay
Thread.Sleep(1);
}
WriteLine("Task-1 is completed.");
}
);
// Starting the task
task1.Start();
// Approach-2
Task task2 = Task.Run(
() =>
{
WriteLine("Task-2 starts.");
for (int i = 210; i < 215; i++)

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.

The main thread starts executing.


The main thread is doing some other work...
Task-2 starts.
Task-2 prints 210 Task-1 starts.
Task-1 prints 100 Task-3 starts.
Task-3 prints 320 Task-2 prints 211 Task-2 prints 212 Task-3 prints 321
Task-3 prints 324 Task-1 prints 104 Task-2 is completed.
Task-1 is completed.
Task-3 is completed.

Q&A Session

Q1.1 What are the benefits of using tasks over threads?

Here are some common benefits:

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 Start method starts System.Threading.Tasks.Task, scheduling it


for execution to the specified System.Threading.Tasks.TaskScheduler.
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 provides the following suggestions (see
https://learn.microsoft.com/en-us/dotnet/standard/parallel-
programming/task-based-asynchronous-programming):

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:

Creation and scheduling don’t have to be separated, and you re-


quire additional task creation options or the use of a specific
scheduler.
You need to pass an additional state into the task that you can re-
trieve through its Task.AsyncState property.

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:

public Task StartNew(


Action action,
CancellationToken cancellationToken,
TaskCreationOptions creationOptions,
TaskScheduler scheduler)
{
// Some code
}
And you can use it as follows:
var printHelloTask = Task.Factory.StartNew(
() =>
{
WriteLine("Hello!");
},
CancellationToken.None,
TaskCreationOptions.AttachedToParent,
TaskScheduler.Default
);

But, in case you use the Run method, a similar option is not available for
you.

Passing and Returning Values

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:

static void PrintNumbers(int limit)


{
for (int i = 0; i < limit; i++)
{
Write($"PrintNumbers prints {i}\n");
// Doing remaining things, if any
Thread.Sleep(1);
}
}

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:

var task1 = new Task(() => PrintNumbers(10));


task1.Start();

Alternatively, you can use the following line:

var task2=Task.Factory.StartNew(() => PrintNumbers(10));

or the following line:

var task3 = Task.Run(() => PrintNumbers(10));

Notice that in either case, you pass the lambda expression:

() => 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.

Figure 1-1 The overloaded versions of the Task constructors

Notice the constructor that is highlighted:

public Task(Action<object?> action, object? state);

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:

public Task StartNew(Action<object?> action, object? state)


{

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:

static void PrintNumbersVersion2(object? state)


{
int limit = Convert.ToInt32(state);
for (int i = 0; i < limit; i++)
{
Write($"PrintNumbers prints {i}\n");
// Doing remaining things, if any
Thread.Sleep(1);
}
}

This time you can write the following:

var task3 = new Task(PrintNumbersVersion2,10);


task3.Start();

Or the following:

var task4 = Task.Factory.StartNew(PrintNumbersVersion2,10);

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.

NoteYou can download the project Demo_PassingValues to experience


the different approaches that you have seen up until now.

Returning Values from Tasks

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 …

var task1 = Task<int>.Factory.StartNew(() => 100);


var result = task1.Result;
WriteLine(result);

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:

var task1 = Task.Factory.StartNew(() => 100);


var result = task1.Result;
WriteLine(result);

Similarly, given the following method:

static int Add(int number1, int number2) => number1 + number2;

you can write the following:

var task2 = Task.Factory.StartNew(() => Add(5, 7));


var result2 = task2.Result;
WriteLine(result2);

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.

NoteI could use Task.Run in this demonstration as well. However, from


Chapter 2 onward, I’ll use Task.Run heavily in this book. Before that, I
am intentionally showing a few demonstrations using
Task.Factory.StartNew in this chapter. It is because I want you to be fa-
miliar with both of them.

using static System.Console;


WriteLine("Passing and returning values by executing tasks.");
static string CalculateFactorial(int number)
{
int temp = Enumerable
.Range(1, number)
.Aggregate((x, y) => x * y);
return $"The factorial of {number} is {temp}";
}
static int Add(int number1, int number2) => number1 + number2;
var task1 = Task.Factory.StartNew(() => CalculateFactorial(5));
var task2 = Task.Factory.StartNew(() => Add(25, 17));
var result1 = task1.Result;
WriteLine(result1);
var result2 = task2.Result;
WriteLine($"The sum of 25 and 17 is {result2}");
WriteLine($"The main thread is completed.");

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:

Passing and returning values by executing tasks.


The factorial of 5 is 120
The sum of 25 and 17 is 42
The main thread is completed.

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.

You can invoke a single as well as multiple continuation tasks.


You can control the continuation. For example, if there are three
tasks, called Task-A, Task-B, and Task-C, you can decide that Task-C
should continue only after both Task-A and Task-B finish their execu-
tions. Alternatively, you may decide that Task-C should not wait for
both Task-A and Task-B; instead, it should continue when any of them
finish the execution.
You can pass data as well as exceptions to the continuation task.
You can also cancel a continuation task if you want. This is often
useful during an emergency or when you find a typical bug that keeps
occurring during the execution of an application.

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

Here is the complete demonstration:

using static System.Console;


var task1 =
Task.Factory.StartNew(() => WriteLine("Ordering food."));
var task2 =
task1.ContinueWith((t) => WriteLine("Inviting friends."));
var task3=
task2.ContinueWith((t) => WriteLine("Arranging dinner."));
task3.Wait();

Output

Here is the output:

Ordering food.
Inviting friends.
Arranging dinner.

Specialized Continuation

In the previous demonstration, task3 followed task2, which in turn, fol-


lowed task1. This flow guaranteed that task3 could not be completed be-
fore task1. However, if task2 runs independently and does not follow
task1, there is no guarantee that task3 will be completed after task1 (it
is because task3 follows task2 but not task1). So, if you want to confirm
that task3 should always continue only after the completion of other
tasks, you need to have more control over the continuation process. Let’s
examine this type of specialized continuation with some case studies.

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:

public Task ContinueWhenAll<TAntecedentResult>


(
Task<TAntecedentResult>[] tasks,
Action<Task<TAntecedentResult>[]> continuationAction
)
{
// Method body not shown
}

Here, the first parameter accepts an array of antecedent tasks (which


means that these need to be finished before you continue), and the next
parameter is for the Action delegate that will execute when all tasks in
the array have been completed.

This is why you’ll see the following line that indicates that task1 and task2
must be completed before you start a continuation task:

var task3 = Task.Factory.ContinueWhenAll(


new [] { task1, task2 },
tasks =>
{
WriteLine("Arranging dinner.");
}
);

Demonstration 4

Let’s see the complete program now:

var task1 = Task.Factory.StartNew(() => WriteLine("Ordering food."));


var task2 = Task.Factory.StartNew(() => WriteLine("Inviting
friends."));
var task3 = Task.Factory.ContinueWhenAll(
new [] { task1, task2 },
tasks =>
{
WriteLine("Arranging dinner.");
}
);
task3.Wait();

Output

Here is some possible output where food is ordered at the beginning:

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):

// Using C#12's "Collection expression" feature


var task3 = Task.Factory.ContinueWhenAll(
[task1, task2],
tasks =>
{
WriteLine("Arranging dinner.");
}
);

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:

CS1593 Delegate 'Action<Task[]>' does not take 0 arguments

But this parameter is useful when you want to analyze the individual tasks
that were finished before. For example, see the following program:

#region For Q&A session


var task1 = Task.Factory.StartNew(() => "Ordering food.");
var task2 = Task.Factory.StartNew(() => "Inviting friends.");
var task3 = Task.Factory.ContinueWhenAll(
new Task<string>[] { task1, task2 },
//new [] { task1, task2 }, // Ok too
// [task1,task2], // C#12 onwards
tasks =>
{
foreach (var task in tasks)
{
WriteLine(task.Result);
}
WriteLine("Arranging dinner.");
}
);
task3.Wait();

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

This code can produce the following output:

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.

NoteYou can download the project Demo4_SpecializedContinuation


from the Apress website to test Demonstration 4 and the Q&A.

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

Let’s see the complete program now:

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 …

using static System.Console;


var task1 = Task.Factory.StartNew(
() =>
{
Thread.Sleep(10);
WriteLine("Ordering food.");
}
);
var task2 = Task.Factory.StartNew(
() =>
{
Thread.Sleep(100);
WriteLine("Inviting friends.");
}
);
var task3 = Task.Factory.ContinueWhenAny(
new[] { task1, task2 },
// [task1, task2], // C#12 onwards
tasks =>
{
WriteLine("Arranging dinner.");
}
);
task3.Wait();

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.

using static System.Console;


var parent = Task.Factory.StartNew(
() =>
{
WriteLine("The parent task has started.");
var child = Task.Factory.StartNew(
() =>
{
WriteLine("The child task has started.");
// Forcing some delay
Thread.Sleep(1000);
WriteLine("The child task has finished.");
});
WriteLine("The parent task has finished now.");
}
);
parent.Wait();

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:

The parent task has started.


The parent task has finished now.
The child task has started.

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

The previous information is self-explanatory. You understand that in the


previous code, the main thread waits for the parent task to finish, but it
does not wait for the child task. To create the parent-child relationship,
we need to attach the child task to the parent. Now the question is: how
can we create such a relationship? See the following program that
demonstrates how to use an overloaded version of the StartNew method
to accept the following argument:
TaskCreationOptions.AttachedToParent.

Demonstration 6

Here is a sample program with the key changes in bold:

using static System.Console;


var parent =
Task.Factory.StartNew(
() =>
{
WriteLine("The parent task has started.");
var child = Task.Factory.StartNew(
() =>
{
WriteLine("The child task has started.");
// Forcing some delay
Thread.Sleep(1000);
WriteLine("The child task has finished.");
},TaskCreationOptions.AttachedToParent);
WriteLine("The parent task has finished now.");
}
);
parent.Wait();

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 …

The parent task has started.


The parent task has finished now.
The child task has started.
The child task has finished.

Q&A Session

Q1.5 In this demonstration, you have written this:

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:

var statusChecker = child.ContinueWith(


task =>
{
WriteLine($"Task id {task.Id}'s status is {task.Status}.");
}, TaskContinuationOptions.AttachedToParent);

In this case, I use an overloaded version of the ContinueWith method where


the first parameter accepts an Action<Task> and the next parameter indicates
the option for when the continuation is scheduled and how it behaves.
Figure 1-2 shows the various continuation options in Visual Studio.

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 …

Figure 1-2 The members of TaskContinuationOptions


If you expand these options, you can see more detail in them. For example, I
can expand AttachedToParent to get the information shown in Figure 1-3.

Figure 1-3 The detail of AttachedToParent in TaskContinuationOptions

Demonstration 7

Let’s see an implementation. The following program is a modified version


of the previous demonstration where we verify the status of the child task
using the Status property. Notice the key changes in bold.

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.

using static System.Console;


var parent = Task.Factory.StartNew(
() =>
{
WriteLine($"The parent task[id:{Task.CurrentId}] has
started.");
var child = Task.Factory.StartNew(
() =>
{
WriteLine($"The child task[id:
{Task.CurrentId}] has started.");
Thread.Sleep(1000);
WriteLine("The child task has finished.");
}, TaskCreationOptions.AttachedToParent);
var statusChecker = child.ContinueWith(
task =>
{

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

Here is some sample output with the important changes in bold:

The parent task[id:9] has started.


The parent task has finished now.
The child task[id:10] has started.
The child task has finished.
Task id 10's status is RanToCompletion.

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.

Figure 1-4 Different possible states of a Task instance

In a concurrent environment, it is possible that by the time you receive the


value of a task status, the status has changed. The interesting point is that once a
state is reached, it does not go back to a previous state. For example, once a task
reaches a final state, it cannot go back to the Created state. Interestingly, there
are three possible final states as follows:

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 …

intervention, timeouts, or any other application logic. I’ll discuss cancella-


tions and exceptions in the next chapter.

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:

The parent task[id:1] has started.


The parent task has finished now.
The child task[id:2] has started.
The child task has finished.
Task id 2's status is RanToCompletion.

Author’s Note: To choose your preferred configuration, you can follow


these steps: right-click the Solution Explorer, select Configuration
Properties and Configuration, and then choose Debug or Release
configuration.

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:

var successHandler = child.ContinueWith(


task =>
{
WriteLine($"Task id: {task.Id} finished successfully");
},
TaskContinuationOptions.AttachedToParent |
TaskContinuationOptions.OnlyOnRanToCompletion);
var cancelHandler = child.ContinueWith(
task =>
{
WriteLine($"Task id:{task.Id} was canceled.");

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).

Here is the complete program:

using static System.Console;


var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var parent = Task.Factory.StartNew(
() =>
{
WriteLine("The parent task has started.");
var child = Task.Factory.StartNew(
() =>
{
WriteLine($"The child task[id:
{Task.CurrentId}] has started.");
Thread.Sleep(5000);
token.ThrowIfCancellationRequested();
WriteLine("The child task has finished.");
},
token,
TaskCreationOptions.AttachedToParent,
TaskScheduler.Default
);
var successHandler = child.ContinueWith(
task =>
{
WriteLine($"Task id: {task.Id} finished
successfully.");
},
TaskContinuationOptions.AttachedToParent |
TaskContinuationOptions.OnlyOnRanToCompletion);
var cancelHandler = child.ContinueWith(
task =>
{
WriteLine($"Task id: {task.Id} was canceled.");
},
TaskContinuationOptions.AttachedToParent |
TaskContinuationOptions.OnlyOnCanceled);
WriteLine("The parent task has finished now.");
}
);
WriteLine("Press 'c' immediately to cancel the child task.");
char ch = ReadKey().KeyChar;
if (ch == 'c')
{
WriteLine("\nTask cancellation requested.");
tokenSource.Cancel();
}
parent.Wait();

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:

Press 'c' immediately to cancel the child task.


The parent task has started.
The parent task has finished now.
The child task[id:2] has started.
The child task has finished.
Task id: 2 finished successfully.

Here is another sample output when the user raised a request to cancel the
child task:

Press 'c' immediately to cancel the child task.


The parent task has started.
The parent task has finished now.
The child task[id:2] has started.
c
Task cancellation requested.
Task id: 2 was canceled.

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:

using static System.Console;


WriteLine("The main thread starts.");
var task1 = Task.Run(
() =>
{
WriteLine("Task1 starts.");
WriteLine("Task1 ends.");
}
);
var task2 = Task.Run(
() =>
{
WriteLine("Task2 starts.");

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

Here are two possible outputs.

Output 1:

The main thread starts.


The end of main.

Output 2:

The main thread starts.


The end of main.
Task1 starts.

Analysis

These outputs reflect the following characteristics:

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:

// The previous code is the same


Thread.Sleep(1000);
WriteLine("The end of main.");

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 main method starts.


Task2 starts.
Task1 starts.
Task1 ends.
Task2 ends.
The end of main.

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!

We do not want unnecessary waiting. At the same time, we do not want to


miss any key information. From this point of view, this is an inefficient
approach. In fact, the situation can be worse if you work on an applica-
tion that tries to block the UI. This is why relying on the Sleep method
may not always be a good idea.

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:

The main thread starts.


Task1 starts.
Task1 ends.
The end of main.
Task2 starts.
Task2 ends.

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.

NoteUsing the async and await keywords simplifies asynchronous pro-


gramming. They are explained in Chapter 6. You do not need to worry
about them now.

Using ReadKey()

Sometimes you will see the presence of ReadKey(), Read(), or ReadLine() in


a program. The basic idea of using these methods is to block control of execution
until the user provides the required input. For example, you can wait for task1
and task2 to finish, and then you can press a key from the keyboard to get the
final output. Here is some sample code:

// The previous code is the same


ReadKey();
WriteLine("The end of main");

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:

// The previous code is the same


task1.Wait();
task2.Wait();
WriteLine("The end of main.");

Here is some sample output after this change:

The main thread starts.


Task2 starts.
Task2 ends.
Task1 starts.
Task1 ends.
The end of main.

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:

// The previous code is the same


Task.WaitAll(task1, task2);

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.

NoteYou can download the project Demo9_DiscussionOnWaiting from


the Apress website to run and validate all these program segments on
your computer.

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:

// The previous code is the same


Task.WaitAny(task1, task2);
WriteLine("The end of main.");

To show you the effect of this change, let me introduce a small delay in task2
as follows:

var task2 = Task.Run(


() =>
{
WriteLine("Task2 starts.");
// A small delay is introduced here
Thread.Sleep(10);
WriteLine("Task2 ends.");
}
);

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:

The main thread starts.


Task1 starts.
Task1 ends.
The end of main.
Task2 starts.

Output-2:

The main thread starts.


Task1 starts.
Task1 ends.
The end of main.

POINTS TO NOTE
Remember the following points:

You may see different output on your computer.


These methods have various overloads. For example, at the time of
this writing, the Wait method has six different overloads. Using these

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 …

overloaded versions, you can provide a maximum duration to wait, a


CancellationToken instance, or both of them to monitor while
waiting.

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.

Consider the following code where I replace WaitAny with WhenAny:

// The previous code is the same


Task.WhenAny(task1, task2);
WriteLine("The end of main.");

Here is some sample output where you can see that the main thread
completes before task1 and task2:

The main thread starts.


Task1 starts.
The end of main.
Task1 ends.
Task2 starts.

To make the concept clearer, let me apply some modifications to the existing
program as follows:

// The previous code is the same


var task=Task.WhenAny(task1, task2);
WriteLine("The end of main.");
WriteLine($"Task1's ID: {task1.Id} Task2's ID: {task2.Id}");
WriteLine($"Completed task id: {task.Result.Id}");

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:

The main thread starts.


The end of main.
Task1's ID: 1 Task2's ID: 2
Task1 starts.
Task1 ends.
Completed task ID: 1
Task2 starts.

NoteYou can download the project


Demo10_DiscussingWaitAnyAndWhenAny from the Apress website to run
and validate the new program segments on your computer.

Waiting for Cancellation

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

Q1.9 In some 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 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.

Note.NET Framework classes such as Monitor and ReaderWriterLock in-


ternally use the SpinWait method. Still, instead of using this method di-
rectly, Microsoft recommends that you use the built-in synchronization
classes to serve your purpose. (I discuss different synchronization tech-
niques in Chapter 3 and Appendix A.) I also recommend not using this
method for one typical reason: SpinWait accepts an integer argument
that represents the number of iterations for the CPU loop to be per-
formed. As a result, the waiting time depends on the processor’s speed.
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 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:

// Previous code is the same.


SpinWait.SpinUntil(() => task1.Status == TaskStatus.RanToCompletion);
WriteLine("The end of main.");

Here is some possible output after this change is made to this program:

The main thread starts.


Task2 starts.
Task1 starts.
Task1 ends.
The end of main.

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.

Q1.10 “These methods have various overloaded versions as well.”


How can I know about them?

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.

Figure 1-6 The Sleep method has two overloaded versions

Q1.11 Can you give me an example where you’d use WhenAny or


WaitAny. Between these two methods, which one would you like to
use?

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.

Q1.12 Between Thread.Sleep and Task.Delay, which one do you


prefer?

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.

Once you’re familiar with async/await programming, you’ll learn that


you can write something like the following: await Task.Delay(1000);
however, you cannot write something like await Task.Sleep(10);.

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:

Task task= Task.Delay(1000);


// Do something here
await task;

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

Check your understanding by attempting the following 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

The same comment applies to all the exercises in this book.

E1.1 Can you predict the output of the following program?

using static System.Console;


Task printHelloTask = new (
() => WriteLine("Hello!")
);
WriteLine("End.");

E1.2 Can you predict the output of the following program?

using static System.Console;


Task printHelloTask = new(
() => WriteLine("Hello!")
);
printHelloTask.Start();
WriteLine("End.");

E1.3 Can you predict the output of the following program?

using static System.Console;


var saySomething = (string msg = "Hello") => msg;
var printHelloTask = Task.Factory.StartNew(
() =>
{
Thread.Sleep(1000);
WriteLine(saySomething());
}

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.4 Can you predict the output of the following program?

using static System.Console;


var calculate = Task.Run(() => GetTotal(5));
WriteLine(calculate.Result);
WriteLine("End.");
static int GetTotal(int count)
{
int total = 0;
for (int i = 1; i <= count; i++)
{
total += i*2;
}
return total;
}

E1.5 Starting with C# 12, we can define a primary constructor as a part of the
class declaration. Here is an example:

class Employee( string name,int id)


{
private string _name=name;
private int _id = id;
public override string ToString()
{
return $"Name:{_name} Id:{_id}";
}
}

You can create an instance of the Employee class as follows:

Employee emp = new("Bob", 1);

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.)

E1.6 Consider two websites such as www.google.com and www.yahoo.com.


You may know that a hostname can be associated with multiple IP ad-
dresses. Can you write a program that involves multiple tasks where one
task prints the IP addresses associated with the hostnames and another
task pings these websites and displays the result? (Assume that your com-
puter is already connected to the Internet. In addition, you do not need to
handle exceptions or cancellations for this exercise.)

E1.7 Can you predict the output of the following program?

using static System.Console;


var helloTask = Task.Run(() =>
{
WriteLine("Hello reader!");
var aboutTask = Task.Factory.StartNew(() =>
{

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();

E1.8 State True/False:

i) There are three possible final states of a task: RanToCompletion,

Canceled, and Faulted.


ii) When you create a child task inside a parent task, it is detached by
default.
iii)A parent task always waits for its child task to finish.
iv)Waiting for a task completion blocks the calling thread.
v) The WaitAny method blocks the calling thread, but the WhenAny

method does not block the calling thread.

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.

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 and return a value from a task?
How can you implement simple task continuation as well as a con-
ditional 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 employ a waiting mechanism to ensure a task fin-
ishes its execution before the application ends?

Solutions to Exercises

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

E1.1

The program will output the following:

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:

using static System.Console;


Task printHelloTask = new(
() =>
{
Thread.Sleep(1000);
WriteLine("Hello!");
}
);
printHelloTask.Start();
WriteLine("End.");

E1.3

C# 12 allows you to define default values for parameters on lambda expressions.


So, the code will compile without any issues. As a result, 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!
End.

E1.4

You will see the following output:

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:

using static System.Console;


var createEmp = Task.Factory.StartNew(
() =>
{
Employee emp = new("Bob", 1);
WriteLine($"Created an employee with {emp}");
}
)
.ContinueWith(
task =>
{
WriteLine($"Was the previous task completed?
{task.IsCompletedSuccessfully}");
WriteLine($"Current time:{DateTime.Now}");
}

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}";
}
}

Here is some sample output:

Created an employee with Name: Bob Id: 1


Was the previous task completed? True
Current time:12/24/2023 12:06:25 PM

E1.6

Here is a sample program that fulfills the criteria:

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);
}

Here is some sample output:

www.google.com's IP addresses are:


2404:6800:4009:826::2004
142.250.183.196
--------------
www.yahoo.com's IP addresses are:
2406:8600:f040:1fa::3000

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:

List<string> websites = ["www.google.com", "www.yahoo.com"];

This line of code is equivalent to the following:

List<string> websites = new()


{
"www.google.com",
"www.yahoo.com"
};

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

On my computer, this program shows the following output:

Hello reader!

This is because the application terminated before aboutTask finishes its


execution. Using the ReadKey or ReadLine() method you can hold the control
until you see the following output:

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):

var helloTask = Task.Factory.StartNew(() =>


{
WriteLine("Hello reader!");
var aboutTask = Task.Factory.StartNew(() =>
{
Task.Delay(1000);
WriteLine("How are you?");
}, TaskCreationOptions.AttachedToParent);
});
helloTask.Wait();

You can expect to see the following output:

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

The answers are as follows:

i) There are three possible final states of a task- RanToCompletion,

Canceled, and Faulted. [True]


ii) When you create a child task inside a parent task, it is detached by
default.[True]
iii)A parent task always waits for its child task to finish. [False]

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

method does not block the calling thread. [True]

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

You might also like