- Judging Task Status
- On Parent-Child Tasks
- Combination Tasks/Continuation Tasks
- Complex Continuation Tasks
- Parallel (Asynchronous) Task Processing
- Parallel (Synchronous) Task Processing
- Parallel Task's Task.WhenAny
- Parallel Task Status
- Value Change Issues in Loops
- Scheduled Task TaskScheduler Class
In the previous article, we learned the basics of tasks, various ways to schedule and execute tasks, and how to asynchronously retrieve return results. This article will focus solely on code practice and example operations.
Judging Task Status
| Property | Description |
|--------|--------|
| IsCanceled | Gets a value indicating whether this Task instance has completed due to being canceled. |
| IsCompleted | Gets a value indicating whether the task has completed. |
| IsCompletedSuccessfully | Indicates whether the task has completed successfully. |
| IsFaulted | Gets a value indicating whether this Task has completed due to an unhandled exception. |
| Status | Gets the TaskStatus of this task. |
To detect if a task has failed (meaning it has terminated due to an unhandled exception), you should use the IsCanceled
and IsFaulted
properties. If the task throws an exception, IsFaulted
will be true. However, canceling a task actually throws an OperationCancelException
, which does not imply that the task failed.
Even when a task throws an unhandled exception, it is still considered complete, so the IsCompleted
property will be true.
Here is an example:
The code is somewhat lengthy, making it difficult to view; please copy it to your program and run it.
class Program
{
static void Main()
{
// Normal task
Task task1 = new Task(() =>
{
});
task1.Start();
Thread.Sleep(TimeSpan.FromSeconds(1));
GetResult(task1.IsCanceled, task1.IsFaulted);
Console.WriteLine("Task completed: " + task1.IsCompleted);
Console.WriteLine("-------------------");
// Exception task
Task task2 = new Task(() =>
{
throw new Exception();
});
task2.Start();
Thread.Sleep(TimeSpan.FromSeconds(1));
GetResult(task2.IsCanceled, task2.IsFaulted);
Console.WriteLine("Task completed: " + task2.IsCompleted);
Console.WriteLine("-------------------");
Thread.Sleep(TimeSpan.FromSeconds(1));
CancellationTokenSource cts = new CancellationTokenSource();
// Cancel the task
Task task3 = new Task(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(3));
}, cts.Token);
task3.Start();
cts.Cancel();
Thread.Sleep(TimeSpan.FromSeconds(1));
GetResult(task3.IsCanceled, task3.IsFaulted);
Console.WriteLine("Task completed: " + task3.IsCompleted);
Console.ReadKey();
}
public static void GetResult(bool isCancel, bool isFault)
{
if (!isCancel && !isFault)
Console.WriteLine("No exception occurred.");
else if (isCancel)
Console.WriteLine("Task was canceled.");
else
Console.WriteLine("Task raised an unhandled exception.");
}
}
On Parent-Child Tasks
In the previous article “C# Multithreading (13): Task Basics I”, we learned about parent-child tasks, where the parent task must wait for the child task to complete for it to be considered finished.
The last chapter only provided examples but did not clarify scenarios and experimental results, so here is a new example for clarification.
Non-parent-child tasks:
The outer task does not wait for the embedded task to complete and simply completes or returns a result.
static void Main()
{
// Two tasks have no hierarchical relationship; they are independent
Task<int> task = new Task<int>(() =>
{
// Not a child task
Task task1 = new Task(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
for (int i = 0; i < 10; i++)
{
});
});
task1.Start();
return 0;
});
}
Parent-child tasks:
The parent task waits for the completion of the child task to be considered complete, then returns the result.
static void Main()
{
// Parent-child task
Task<int> task = new Task<int>(() =>
{
// Child task
Task task1 = new Task(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
for (int i = 0; i < 10; i++)
{
});
});
task1.Start();
task1.Wait(); // Wait for the child task to complete
return 0;
});
}
Combination Tasks/Continuation Tasks
The Task.ContinueWith()
method creates a continuation task that executes asynchronously upon the completion of a Task instance.
There are many overloaded methods for Task.ContinueWith()
, which you can refer to here: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.continuewith?view=netcore-3.1#--
Here is the constructor we are using defined as follows:
public Task ContinueWith(Action<task> continuationAction);
A simple example:
Task task = new Task(() =>
{
Console.WriteLine("First Task.");
Thread.Sleep(TimeSpan.FromSeconds(2));
});
// Next, the second task
task.ContinueWith(t =>
{
Console.WriteLine("Second Task.");
Thread.Sleep(TimeSpan.FromSeconds(2));
});
task.Start();
A single task can have multiple continuation tasks that run in parallel, for example:
static void Main()
{
Task task = new Task(() =>
{
Console.WriteLine("First Task.");
Thread.Sleep(TimeSpan.FromSeconds(1));
});
// Task 1
task.ContinueWith(t =>
{
for (int i = 0; i < 10; i++)
{
});
});
task.Start();
}
By implementing continuation/combination tasks multiple times, you can achieve a robust task workflow.
Complex Continuation Tasks
After the last section, we learned about ContinueWith()
for continuing tasks; now let's explore more overload methods to implement more complex continuations.
There are numerous overloads for ContinueWith()
, which parameters include one or more of the following.
-
continuationAction
Type: Action or Func
A task to execute.
-
state
Type: Object
The parameter passed to the continuation task.
-
cancellationToken
Type: CancellationToken
A cancellation token.
-
continuationOptions
Type: TaskContinuationOptions
Controls the creation and characteristics of the continuation task.
-
scheduler
Type: TaskScheduler
A TaskScheduler associated with the continuation task and used during its execution process.
The first four parameters (types) have already been covered in previous articles, so we will not elaborate further; the TaskScheduler
type will be explained later.
Note the distinction between TaskCreationOptions
and TaskContinuationOptions
; we studied TaskCreationOptions
in the previous article. Here we will study TaskContinuationOptions
.
TaskContinuationOptions
can be used in the following overloads:
ContinueWith(Action, CancellationToken, TaskContinuationOptions, TaskScheduler);
ContinueWith(Action, TaskContinuationOptions);
Using it like this in continuations is ineffective:
Task task = new Task(() =>
{
Console.WriteLine("First Task.");
Thread.Sleep(TimeSpan.FromSeconds(1));
});
task.ContinueWith(t =>
{
for (int i = 0; i < 10; i++)
{
});
Because TaskContinuationOptions
requires a nested hierarchical parent-child relationship for it to work.
Correct usage method:
static void Main()
{
// Parent-child task
Task<int> task = new Task<int>(() =>
{
// Child task
Task task1 = new Task(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("Inner task 1");
Thread.Sleep(TimeSpan.FromSeconds(0.5));
}, TaskCreationOptions.AttachedToParent);
task1.ContinueWith(t =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("Inner continuation task, also a child task.");
Thread.Sleep(TimeSpan.FromSeconds(0.5));
}, TaskContinuationOptions.AttachedToParent);
task1.Start();
Console.WriteLine("Outer layer task");
return 666;
});
task.Start();
Console.WriteLine($"Task result is: {task.Result}");
Console.WriteLine("\n-------------------\n");
Console.ReadKey();
}
Parallel (Asynchronous) Task Processing
Here we will learn about the usage of the Task.WhenAll()
method.
Task.WhenAll()
: Waits for all provided Task objects to complete execution.
The usage is as follows:
static void Main()
{
List<Task> tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
tasks.Add(new Task(() =>
{
Console.WriteLine("Task started executing.");
}));
}
// public static Task WhenAll(IEnumerable<Task> tasks);
// Equivalent to multiple tasks generating a single task
Task taskOne = Task.WhenAll(tasks);
// If you don't need to wait, you can skip this
taskOne.Wait();
Console.ReadKey();
}
Task taskOne = Task.WhenAll(tasks);
can be written as Task.WhenAll(tasks);
, and the returned Task object can be used to check the execution status of the tasks.
Note that the following is ineffective:
You can modify the above code to test.
tasks.Add(new Task(() =>
{
Console.WriteLine("Task started executing.");
}));
I also don't know why new Task()
doesn't work...
If tasks have return values, you can use the following method:
static void Main()
{
List<Task<int>> tasks = new List<Task<int>>();
for (int i = 0; i < 5; i++)
{
tasks.Add(new Task<int>(() =>
{
Console.WriteLine("Task started executing.");
return new Random().Next(0, 10);
}));
}
Task<int[]> taskOne = Task.WhenAll(tasks);
foreach (var item in taskOne.Result)
Console.WriteLine(item);
Console.ReadKey();
}
Parallel (Synchronous) Task Processing
Task.WaitAll()
: Waits for all provided Task objects to complete execution.
Let’s take a look at one of the overloaded method definitions for Task.WaitAll()
:
public static bool WaitAll(Task[] tasks, int millisecondsTimeout, CancellationToken cancellationToken);
- tasks Type: Task[]
All tasks to be executed.
- millisecondsTimeout Type: Int32
The number of milliseconds to wait; -1 means wait indefinitely.
- cancellationToken Type: CancellationToken
The CancellationToken to observe while waiting for tasks to complete.
An example of Task.WaitAll()
is as follows:
static void Main()
{
List<Task> tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
tasks.Add(new Task(() =>
{
Console.WriteLine("Task started executing.");
}));
}
Task.WaitAll(tasks.ToArray());
Console.ReadKey();
}
Task.WaitAll()
will make the current thread wait for all tasks to complete. Additionally, Task.WaitAll()
does not have generics and does not return results.
Task.WhenAny for Parallel Tasks
Task.WhenAny()
and Task.WhenAll()
are similar in use. Task.WhenAll()
completes only when all tasks are finished, while Task.WhenAny()
completes as soon as any one of the tasks finishes.
This can be referenced in the parent-child tasks discussed above.
Here is a sample usage:
static void Main()
{
List<task> tasks = new List<task>();
for (int i = 0; i < 5; i++)
{
tasks.Add(Task.Run(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(new Random().Next(0, 5)));
Console.WriteLine("正在执行任务");
}));
}
Task taskOne = Task.WhenAny(tasks);
taskOne.Wait(); // Any one task completes, and we can release the wait.
Console.WriteLine("有任务已经完成了");
Console.ReadKey();
}
Of course, Task.WhenAny()
also has a generic method that can return results.
Parallel Task Status
The Task.Status
property can retrieve the status of a task. Its property type is a TaskStatus
enumeration defined as follows:
| Enumeration | Value | Description |
|-------------|-------|-------------|
| Canceled | 6 | The task has been canceled via CancellationToken. |
| Created | 0 | The task has been initialized, but not yet scheduled. |
| Faulted | 7 | The task completed due to an unhandled exception. |
| RanToCompletion | 5 | The task completed successfully. |
| Running | 3 | The task is currently running but not yet completed. |
| WaitingForActivation | 1 | The task is waiting for the .NET Framework infrastructure to internally activate and schedule it. |
| WaitingForChildrenToComplete | 4 | The task has completed execution and is implicitly waiting for additional child tasks to complete. |
| WaitingToRun | 2 | The task has been scheduled but has not yet started executing. |
When using parallel tasks, the Task.Status
value has certain patterns:
- If any of the tasks experiences an unhandled exception, it will return
TaskStatus.Faulted
. - If all tasks have unhandled exceptions, it will return
TaskStatus.RanToCompletion
. - If any task is canceled (even if it has unhandled exceptions), it will return
TaskStatus.Canceled
.
Value Change Issues in Loops
Please run the tests for the following two examples:
static void Main()
{
for (int i = 0; i < 5; i++)
{
Task.Run(() =>
{
Console.WriteLine($"i = {i}");
});
}
Console.ReadKey();
}
static void Main()
{
List<task> tasks = new List<task>();
for (int i = 0; i < 5; i++)
{
tasks.Add(Task.Run(() =>
{
Console.WriteLine($"i = {i}");
}));
}
Task taskOne = Task.WhenAll(tasks);
taskOne.Wait();
Console.ReadKey();
}
You will find that the results of the two examples are not 1,2,3,4,5
, but rather 5,5,5,5,5
.
This issue is known as a Race Condition. You can refer to Wikipedia:
https://en.wikipedia.org/wiki/Race_condition
There is also an explanation of this issue in the Microsoft documentation, please refer to:
Since i
resides in the same memory location throughout its lifecycle, each thread or task points to the same location for its value.
This works:
static void Main()
{
for (int i = 0; i < 5; i++)
{
int tmp = i; // Capture the value of i
Task.Run(() =>
{
Console.WriteLine($"i = {tmp}");
});
}
Console.ReadKey();
}
This is ineffective:
for (int i = 0; i < 5; i++)
{
Task.Run(() =>
{
Console.WriteLine($"i = {i}");
});
}
Timed Tasks with TaskScheduler Class
The TaskScheduler
class represents an object that handles scheduling tasks to threads on a low level.
Most examples online are related to WPF and WinForm
, and the Microsoft documentation appears quite complicated: https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskscheduler?view=netcore-3.1#properties
It seems the TaskScheduler
mainly controls the SynchronizationContext
, meaning it influences the UI.
Since the author does not write WPF or WinForms, this topic will be skipped. Ha ha ha.
文章评论