Multithreaded Programming
Multithreaded Programming Models
In .NET, there are three asynchronous programming models: Task-based Asynchronous Pattern (TAP), Event-based Asynchronous Pattern (EAP), and Asynchronous Programming Model (APM).
- Task-based Asynchronous Pattern (TAP): The recommended asynchronous programming method in .NET, this model uses a single method to represent the start and completion of asynchronous operations. It includes commonly used keywords such as async and await, which support this model.
- Event-based Asynchronous Pattern (EAP): An older model that provides asynchronous behavior based on events. This model is mentioned in “C# Multithreading (12): Thread Pool”, and it is not supported in .NET Core.
- Asynchronous Programming Model (APM): Also known as IAsyncResult, it is an older model that provides asynchronous behavior using the IAsyncResult interface. .NET Core also does not support this; please refer to “C# Multithreading (12): Thread Pool”.
Previously, we studied three parts:
- Thread Basics: How to create threads, obtain thread information, and wait for threads to complete tasks;
- Thread Synchronization: Exploring various methods for achieving process and thread synchronization, as well as thread waiting;
- Thread Pool: The advantages and usage of thread pools, task-based operations;
This article begins to explore tasks and asynchronous operations, which are quite complex and intricate. The author may not be able to explain it well...
Exploring Advantages
In the previous sections, we wrote a total of 10 articles on multithreading (thread basics and thread synchronization), so much code has been written, and now let’s explore the complexity of multithreaded programming.
-
Data Passing and Result Returning
Passing data is not a big issue, but it is challenging to obtain the return value from a thread, and handling thread exceptions requires skill.
-
Monitoring Thread States
After creating a new thread, if you need to determine when the new thread has completed, you need to spin or block to wait.
-
Thread Safety
Design should consider how to avoid deadlocks, appropriately use various synchronization locks, and consider atomic operations; the handling of synchronization signals requires technique.
-
Performance
The primary need for multithreading is to improve performance; however, there are many pitfalls in multithreading that, if misused, could adversely affect performance.
[The above summary can refer to “C# 7.0 Essence”, Section 19.3, and “C# 7.0 Core Technology Guide”, Section 14.3.] By using a thread pool, we can solve some of the above problems, but an even better option is the Task. Additionally, Task is also the foundational type for asynchronous programming, and much of the subsequent content will revolve around Task.
For theoretical aspects, it is advisable to refer to Microsoft’s official documentation and books, as the author may not be entirely accurate and will not delve too deeply into these.
Task Operations
There are many APIs related to tasks (Task), and there are various intricate operations to clarify. It’s truly not easy to explain everything; we will take it slow and improve step by step, writing more code and testing.
Now, let’s familiarize ourselves with the Task API step by step.
Two Ways to Create Tasks
You can create a task using its constructor, defined as follows:
public Task (Action action);
Here is an example:
class Program
{
static void Main()
{
// Define two tasks
Task task1 = new Task(() =>
{
Console.WriteLine("① 开始执行");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("① 执行中");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("① 执行即将结束");
});
Task task2 = new Task(MyTask);
// Start tasks
task1.Start();
task2.Start();
Console.ReadKey();
}
private static void MyTask()
{
Console.WriteLine("② 开始执行");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("② 执行中");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("② 执行即将结束");
}
}
The .Start()
method is used to start a task. Microsoft documentation explains: Start the Task and schedule it for execution in the current TaskScheduler.
We will discuss TaskScheduler later, no need to rush.
Another way to create tasks is to use Task.Factory
, which is a factory method for creating and configuring Task
and Task<TResult>
instances.
You can add tasks using Task.Factory.StartNew()
, which has many overloads; you can refer to: https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--.
Here is the example using two overloads:
public Task StartNew(Action action);
public Task StartNew(Action action, TaskCreationOptions creationOptions);
The example code is as follows:
class Program
{
static void Main()
{
// Overload method 1
Task.Factory.StartNew(() =>
{
Console.WriteLine("① 开始执行");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("① 执行中");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("① 执行即将结束");
});
// Overload method 1
Task.Factory.StartNew(MyTask);
// Overload method 2
Task.Factory.StartNew(() =>
{
Console.WriteLine("① 开始执行");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("① 执行中");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("① 执行即将结束");
}, TaskCreationOptions.LongRunning);
Console.ReadKey();
}
// public delegate void TimerCallback(object? state);
private static void MyTask()
{
Console.WriteLine("② 开始执行");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("② 执行中");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("② 执行即将结束");
}
}
Tasks added through the Task.Factory.StartNew()
method will enter the thread pool task queue and execute automatically without manual start.
TaskCreationOptions.LongRunning
is an enumeration that controls the task creation characteristics, which we will discuss later.
Creating Tasks with Task.Run()
Creating tasks with Task.Run()
is similar to Task.Factory.StartNew()
, of course, Task.Run()
also has many overloads and intricate operations that we will learn about later.
Here is sample code for creating a task using Task.Run()
:
static void Main()
{
Task.Run(() =>
{
Console.WriteLine("① 开始执行");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("① 执行中");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("① 执行即将结束");
});
Console.ReadKey();
}
Cancelling Tasks
Cancelling tasks has been discussed in “C# Multithreading (12): Thread Pool”, but the control is very free, relying entirely on the task itself to judge whether to cancel.
Here we implement task cancellation through Task; it is real-time, automatic, and does not require manual control.
Its constructor is as follows:
public Task StartNew(Action action, CancellationToken cancellationToken);
Example code is as follows:
Remember to switch letter modes when you press the Enter key.
class Program
{
static void Main()
{
Console.WriteLine("任务开始启动,按下任意键,取消执行任务");
CancellationTokenSource cts = new CancellationTokenSource();
Task.Factory.StartNew(MyTask, cts.Token);
Console.ReadKey();
cts.Cancel(); // Cancel the task
Console.ReadKey();
}
// public delegate void TimerCallback(object? state);
private static void MyTask()
{
Console.WriteLine("开始执行");
int i = 0;
while (true)
{
Console.WriteLine($"第{i}次任务");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("执行中");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("执行结束");
i++;
}
}
}
Parent-Child Tasks
Previously, while creating tasks, we encountered the TaskCreationOptions.LongRunning
enumeration, which is used to control task creation and set task behavior.
The enumerations are as follows:
| Enumeration | Value | Description |
|--------------------------|-------|-----------------------------------------------------------------------------|
| AttachedToParent | 4 | Specifies that the task is attached to a certain parent in the task hierarchy. |
| DenyChildAttach | 8 | Specifies that any attempt to execute child tasks as attached tasks cannot attach to the parent task and will execute as detached tasks. |
| HideScheduler | 16 | Prevents the environment scheduler from being treated as the current scheduler for the created task. |
| LongRunning | 2 | Specifies that the task will be a long-running, coarse-grained operation, involving fewer, larger components than refined systems. |
| None | 0 | Specifies that the default behavior should be used. |
| PreferFairness | 1 | Hints to the TaskScheduler to schedule tasks as fairly as possible. |
| RunContinuationsAsynchronously | 64 | Forces the asynchronous execution of continuation tasks added to the current task. |
This enumeration can be used with TaskFactory
and TaskFactory<TResult>
, Task
and Task<TResult}
, StartNew()
, FromAsync()
, TaskCompletionSource<TResult>
, etc.
Here we explore the usage of TaskCreationOptions.AttachedToParent
. Here is an example code snippet:
// Parent-Child Tasks
Task task = new Task(() =>
{
// TaskCreationOptions.AttachedToParent
// Attach this task to the parent task
// The parent task needs to wait for all child tasks to complete before it is considered complete
Task task1 = new Task(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
for (int i = 0; i
Conversely, TaskCreationOptions.DenyChildAttach
does not allow other tasks to attach to the outer task.
static void Main()
{
// Disallow parent-child tasks
Task task = new Task(() =>
{
Task task1 = new Task(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
for (int i = 0; i
Moreover, we also learned a new Task method: Wait()
, which waits for the Task to complete execution. Wait()
can also be set with a timeout.
If the parent task is created by calling the Task.Run method, it implicitly prevents child tasks from attaching to it.
For detailed information on attached child tasks, please refer to: https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/attached-and-detached-child-tasks?view=netcore-3.1.
Returning Results from Tasks and Asynchronously Obtaining Results
To obtain results from tasks, you need to create tasks using generic classes or methods, such as Task<TResult>
, Task.Factory.StartNew<TResult>()
, and Task.Run<TResult>()
.
You can obtain the return result through the generic Result
property.
To asynchronously obtain task execution results:
class Program
{
static void Main()
{
// *******************************
Task<int> task = new Task<int>(() =>
{
return 666;
});
// Execute
task.Start();
// Obtain results, which belongs to asynchronous execution
int number = task.Result;
// *******************************
task = Task.Factory.StartNew<int>(() =>
{
return 666;
});
// Can also asynchronously obtain results
number = task.Result;
// *******************************
task = Task.Run<int>(() =>
{
return 666;
});
// Can also asynchronously obtain results
number = task.Result;
Console.ReadKey();
}
}
If you want to execute synchronously, you can change it to:
int number = Task.Factory.StartNew<int>(() =>
{
return 666;
}).Result;
Capturing Task Exceptions
If an exception occurs during an ongoing task, it will not directly throw to prevent the main thread from executing; instead, the exception will be re-thrown when obtaining task results or waiting for the task to complete.
```csharp
static void Main()
{
// *******************************
Task<int> task = new Task<int>(() =>
{
throw new Exception("I just want to throw an exception anyway");
});
// Execute
task.Start();
Console.WriteLine("The exception in the task won't propagate to the main thread.");
Thread.Sleep(TimeSpan.FromSeconds(1));
// When the task encounters an exception, it will be thrown when getting the result
int number = task.Result;
// task.Wait(); If waiting for the task, it will also throw an exception if occurs
Console.ReadKey();
}
Throwing exceptions wildly is not a good practice~ You can change it to:
static void Main()
{
Task<program> task = new Task<program>(() =>
{
try
{
throw new Exception("I just want to throw an exception anyway");
return new Program();
}
catch
{
return null;
}
});
task.Start();
var result = task.Result;
if (result is null)
Console.WriteLine("Task execution failed");
else Console.WriteLine("Task execution succeeded");
Console.ReadKey();
}
Global Exception Handling for Tasks
TaskScheduler.UnobservedTaskException
is an event, and its delegate is defined as follows:
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
Here is an example:
Please publish the program and run it from the directory.
class Program
{
static void Main()
{
TaskScheduler.UnobservedTaskException += MyTaskException;
Task.Factory.StartNew(() =>
{
throw new ArgumentNullException();
});
Thread.Sleep(100);
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Done");
Console.ReadKey();
}
public static void MyTaskException(object sender, UnobservedTaskExceptionEventArgs eventArgs)
{
// eventArgs.SetObserved();
((AggregateException)eventArgs.Exception).Handle(ex =>
{
Console.WriteLine("Exception type: {0}", ex.GetType());
return true;
});
}
}
I'm not quite sure how to use TaskScheduler.UnobservedTaskException, and the effect is hard to observe.
Please refer to:
https://stackoverflow.com/search?q=TaskScheduler.UnobservedTaskException
文章评论