---
title: "C#多线程"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/csharp-multithreading/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/02-编程语言与运行时/.NET与CSharp/C#多线程.md"
content_hash: aaa82a164ee5d19fe6ddae7e4704d2c01fcf907601d348ea7a15302d91187644
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# C#多线程

> 创建时间：2020/12/10 15:57

  * C#多线程
    * concepts
      * Multitasking
        * process
        * Thread
    * Multithreading in C Sharp
      * Thread
        * 状态
        * 启动与参数传递
          * 委托传参(需要装箱拆箱)
          * 使用静态变量或类成员变量
          * 委托 + 两层函数避免Boxing
        * 阻塞（Block）
        * 线程优先级、前台线程和后台线程
      * 多线程竞争
        * lock & Monitor 与线程锁
        * 原子操作
        * Mutex类与进程同步
        * Semaphore（信号量）与资源池
        * ReaderWriterLockSlim
      * 线程间通信与同步
        * AutoRestEvent, ManualResetEvent
        * CountdownEvent 与 线程完成数
        * Barrier 类
        * 线程等待进阶
      * ThreadPool 线程池
        * ThreadPool 类
        * 适用性
        * 具体用法
        * 线程池线程数
        * 取消任务（终止线程）
        * Timer
      * 基于任务的异步模式(Task-based asynchronous pattern)
        * 多线程编程要解决的问题
        * 创建&启动任务
        * 取消任务
        * TaskCreationOptions
          * 父子任务
        * 任务异常与状态判断
        * WhenAll 与 异步并行
        * WhenAny
        * WaitAll 与 同步
        * 延续任务
          * ContinueWith
          * TaskAwaiter
        * async & await 与同步、异步
      * 实现一个既能同步又能异步执行的类
    * Ref

## concepts

### Multitasking

Windows operating system is a multitasking operating system. It means it has the ability to run multiple applications at the same time.

#### process

A process is a part of the operating system (or a component under the operating system) which is responsible for executing the program or application.

![Alt text](/media/9cbeb18d0d6aedd5c9bb.png)

There are also multiple processes that are running in the background which are known as the **background processes**. These background processes are known as windows services and the Operating system runs a lot of windows services in the background.

So, we have an **operating system** and under the operating system, we have **processes** that running our **applications**.

#### Thread

In computer science, a thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler, which is typically a part of the operating system. The implementation of threads and processes differs between operating systems, but in most cases a thread is a component of a process.

In simple words, we can say that a Thread is a unit of a process that is responsible for executing the application code.

## Multithreading in C Sharp

### Thread

#### 状态

操作系统中进程的状态的常见表述：
三种基本状态：就绪，运行，阻塞。
实际系统中，为了管理的需要，还存在着另外两种状态：创建状态和终止状态。

![Alt text](/media/d4652d2c3fd2c7dd7d4d.png)

via: [Studytonight](https://www.studytonight.com/operating-system/operating-system-processes)

  * NEW(创建，未启动状态)- The process is being created.

  * READY（就绪状态）- The process is waiting to be assigned to a processor.

  * RUNNING（运行）- Instructions are being executed.

  * WAITING（阻塞，等待，不可运行状态）- The process is waiting for some event to occur(such as an I/O completion or reception of a signal).

  * TERMINATED（终止，死亡状态）- The process has finished execution.

* * *

C#中的ThreadState图(仅供理解，不建议深究):

![Alt text](/media/4ef3565c668249ac60ca.png)

> Thread state is only of interest in debugging scenarios. Your code should never use thread state to synchronize the activities of threads.

ThreadState 是一个枚举，记录了线程的状态，我们可以从中判断线程的生命周期和健康情况。但是里面有很多枚举类型是没有用处的，我们可以使用一个这样的方法来获取更加有用的信息：

```csharp
string s1 = &quot;Hello &quot;;
string s2 = s1;
s1 += &quot;World&quot;;

System.Console.WriteLine(s2);
//Output: Hello

```

#### 启动与参数传递

##### 委托传参(需要装箱拆箱)

```csharp
string s1 = &quot;Hello &quot;;
string s2 = s1;
s1 += &quot;World&quot;;

System.Console.WriteLine(s2);
//Output: Hello

```

##### 使用静态变量或类成员变量

优点是不需要装箱拆箱，多线程可以共享空间；缺点是变量是大家都可以访问，此种方式在多线程竞价时，可能会导致多种问题(可以加锁解决)。

```csharp
string s1 = &quot;Hello &quot;;
string s2 = s1;
s1 += &quot;World&quot;;

System.Console.WriteLine(s2);
//Output: Hello

```

##### 委托 + 两层函数避免Boxing

```csharp
string s1 = &quot;Hello &quot;;
string s2 = s1;
s1 += &quot;World&quot;;

System.Console.WriteLine(s2);
//Output: Hello

```

#### 阻塞（Block）

阻塞的定义：当线程由于特点原因暂停执行，那么它就是阻塞的。
如果线程处于阻塞状态，线程就会交出他的 CPU 时间片，并且不会消耗 CPU 时间，直至阻塞结束。阻塞会发生上下文切换。

  * Thread.Sleep() 方法可以将当前线程挂起一段时间

  * Thread.Join() 方法可以阻塞当前线程一直等待另一个线程运行至结束。

#### 线程优先级、前台线程和后台线程

Highest > AboveNormal > Normal > BelowNormal > Lowest
前台线程的优先级大于后台线程，并且程序需要等待所有前台线程执行完毕后才能关闭；而当程序关闭时，无论后台线程是否在执行，都会强制退出。

### 多线程竞争

> 关于同步和互斥：
>  相交进程之间的关系主要有两种，同步与互斥。所谓互斥，是指散步在不同进程之间的若干程序片断，当某个进程运行其中一个程序片段时，其它进程就不能运行它 们之中的任一程序片段，只能等到该进程运行完这个程序片段后才可以运行。所谓同步，是指散步在不同进程之间的若干程序片断，它们的运行必须严格按照规定的 某种先后次序来运行，这种先后次序依赖于要完成的特定的任务。
>  显然，同步是一种更为复杂的互斥，而互斥是一种特殊的同步。
>  也就是说互斥是两个线程之间不可以同时运行，他们会相互排斥，必须等待一个线程运行完毕，另一个才能运行，而同步也是不能同时运行，但他是必须要安照某种次序来运行相应的线程（也是一种互斥）！
>  总结：互斥：是指某一资源同时只允许一个访问者对其进行访问，具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序，即访问是无序的。
>  同步：是指在互斥的基础上（大多数情况），通过其它机制实现访问者对资源的有序访问。在大多数情况下，同步已经实现了互斥，特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

#### lock & Monitor 与线程锁

lock 用于读一个引用类型进行加锁，同一时刻内只有一个线程能够访问此对象。锁可以阻止其它线程执行锁块(lock(o){})中的代码，当锁定时，其它线程必须等待锁中的线程执行完成并释放锁。

lock 是语法糖，是通过 Monitor 来实现的。lock的用法及底层实现：

```csharp
string s1 = &quot;Hello &quot;;
string s2 = s1;
s1 += &quot;World&quot;;

System.Console.WriteLine(s2);
//Output: Hello

```
```csharp
class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;
        dyn = dyn + 3;
        //obj = obj + 3; 报错

        // Rest the mouse pointer over dyn and obj to see their
        // types at compile time.
        System.Console.WriteLine(dyn.GetType());
        System.Console.WriteLine(obj.GetType());
    }
}

```

Lock 锁定的对象，应该是 **静态的引用类型** （字符串除外）。

> 锁不太适合I/O场景，例如文件I/O，繁杂的计算或者操作比较持久的过程，会给程序带来很大的性能损失。

一般来说，lock关键字够用了，Monitor用法参见: <https://www.cnblogs.com/whuanle/p/12722853.html>

#### 原子操作

在接受到 **中断（Interrupt）** 的时候，CPU 必须要进行 **上下文交换（Context Switch）** 。进行上下文切换时，会带来性能损失。

> 只有操作系统才能切换线程、挂起线程，因此阻塞线程是由操作系统处理的，这种方式被称为 **内核模式(kernel-mode)** 。

Interlocked 类，为多个线程共享的变量提供原子操作。使用 Interlocked 类，可以在不阻塞线程(lock、Monitor)的情况下，避免竞争条件。
更多信息参见：<https://www.cnblogs.com/whuanle/p/12724371.html>

#### Mutex类与进程同步

Mutex 中文为互斥，Mutex 类叫做互斥锁。它还可用于进程间同步的同步基元。互斥锁(Mutex)，用于多线程中防止两条线程同时对一个公共资源进行读写的机制。

> Mutex 跟 lock 相似，但是 Mutex 支持多个进程。Mutex 大约比 lock 慢 20 倍。

Windows 操作系统中，Mutex 同步对象有两个状态：

  * signaled：未被任何对象拥有；

  * nonsignaled：被一个线程拥有；

Mutex 只能在获得锁的线程中，释放锁。

```csharp
class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;
        dyn = dyn + 3;
        //obj = obj + 3; 报错

        // Rest the mouse pointer over dyn and obj to see their
        // types at compile time.
        System.Console.WriteLine(dyn.GetType());
        System.Console.WriteLine(obj.GetType());
    }
}

```

更详细用法参见：<https://www.cnblogs.com/whuanle/p/12726724.html>

#### Semaphore（信号量）与资源池

限制同时访问某一资源或资源池的线程数。
详见：<https://www.cnblogs.com/whuanle/p/12728416.html>

#### ReaderWriterLockSlim

ReaderWriterLock 类：定义支持单个写线程和多个读线程的锁。
ReaderWriterLockSlim 类：表示用于管理资源访问的锁定状态，可实现多线程读取或进行独占式写入访问。
两者的 API 十分接近， ReaderWriterLockSlim 相对更加安全。
详细内容参见：<https://www.cnblogs.com/whuanle/p/12773894.html>

### 线程间通信与同步

#### AutoRestEvent, ManualResetEvent

用于从一个线程向另一个线程发送通知。

```csharp
class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;
        dyn = dyn + 3;
        //obj = obj + 3; 报错

        // Rest the mouse pointer over dyn and obj to see their
        // types at compile time.
        System.Console.WriteLine(dyn.GetType());
        System.Console.WriteLine(obj.GetType());
    }
}

```

![Alt text](/media/9261be729f09d11cc0cd.png)

* * *

AutoResetEvent 和 ManualResetEvent 十分相似。两者之间的区别，在于前者是自动(Auto)，后者是手动(Manua)。

```csharp
class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;
        dyn = dyn + 3;
        //obj = obj + 3; 报错

        // Rest the mouse pointer over dyn and obj to see their
        // types at compile time.
        System.Console.WriteLine(dyn.GetType());
        System.Console.WriteLine(obj.GetType());
    }
}

```

#### CountdownEvent 与 线程完成数

CountdownEvent Class
Represents a synchronization primitive that is signaled when its count reaches zero.
设定一个计数器，每个线程完成后，就会减去 1 ，当计数器为 0 时，代表所有线程都已经完成了任务。

需求: 假如，程序需要向一个 Web 发送 5 次请求，受网路波动影响，有一定几率请求失败。如果失败了，就需要重试。
传统方法

```csharp
class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;
        dyn = dyn + 3;
        //obj = obj + 3; 报错

        // Rest the mouse pointer over dyn and obj to see their
        // types at compile time.
        System.Console.WriteLine(dyn.GetType());
        System.Console.WriteLine(obj.GetType());
    }
}

```

使用CountdownEvent 实现：

```csharp
class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;
        dyn = dyn + 3;
        //obj = obj + 3; 报错

        // Rest the mouse pointer over dyn and obj to see their
        // types at compile time.
        System.Console.WriteLine(dyn.GetType());
        System.Console.WriteLine(obj.GetType());
    }
}

```

#### Barrier 类

![Alt text](/media/1a1e454359b932188c56.png)

每个参与者完成阶段任务后后将被阻止继续执行，直至所有参与者都已达到同一阶段。

情景: 假设有个比赛，一个有三个环节，有三个小组参加比赛。比赛有三个环节，小组完成一个环节后，可以去等待区休息，等待其他小组也完成比赛后，开始进行下一个环节的比赛。
`new Barrier(int,Action)`设置有多少线程参与，Action 委托设置每个阶段完成后执行哪些动作。

`.SignalAndWait()` 阻止当前线程继续往下执行；直到其他完成也执行到此为止。

```csharp
class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;
        dyn = dyn + 3;
        //obj = obj + 3; 报错

        // Rest the mouse pointer over dyn and obj to see their
        // types at compile time.
        System.Console.WriteLine(dyn.GetType());
        System.Console.WriteLine(obj.GetType());
    }
}

```

#### 线程等待进阶

<https://www.cnblogs.com/whuanle/p/12783086.html>

### ThreadPool 线程池

![Alt text](/media/ab5fba0987e913f26884.png)

线程池全称为托管线程池，线程池受 .NET 通用语言运行时(CLR)管理，线程的生命周期由 CLR 处理，因此我们可以专注于实现任务，而不需要理会线程管理。

#### ThreadPool 类

一个应用程序最多只能有一个线程池；

ThreadPool 类是一个静态类，它提供一个线程池，该线程池可用于执行任务、发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器。
线程池维护一个请求队列， QueueUserWorkItem() 方法接受一个代表用户异步操作的委托(名为 WaitCallback )，调用此方法传入委托后，就会进入线程池内部队列中。
WaitCallback 委托的定义如下：
`public delegate void WaitCallback(object state);`

  * 不要将长时间运行的操作放进线程池中；

  * 不应该阻塞线程池中的线程；

  * 线程池中的线程都是后台线程(又称工作者线程)；

#### 适用性

  * 线程池是为突然大量爆发的线程设计的，通过有限的几个固定线程为大量的操作服务， **减少了创建和销毁线程所需的时间** ，从而提高效率。

  * ThreadPool适合于并发运行若干个运行时间不长且互不干扰的函数。

* * *

ThreadPool中的线程不用手动开始，也不能手动取消，你要做的只是把工作函数排入线程池，剩下的工作由系统自动完成，也就是说我们不能控制线程池中的线程。
在以下情况下 **不宜使用** ThreadPool类而应该使用单独的Thread类：

  * 线程执行需要很长时间；

  * 需要为线程指定详细的优先级；

  * 在执行过程中需要对线程进行操作，比如睡眠，挂起等。

#### 具体用法

```csharp
class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;
        dyn = dyn + 3;
        //obj = obj + 3; 报错

        // Rest the mouse pointer over dyn and obj to see their
        // types at compile time.
        System.Console.WriteLine(dyn.GetType());
        System.Console.WriteLine(obj.GetType());
    }
}

```

> Environment.ProcessorCount 可以确定当前计算机上有多少个处理器数量(例如CPU是四核八线程，结果就是八)。

#### 线程池线程数

```csharp
class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;
        dyn = dyn + 3;
        //obj = obj + 3; 报错

        // Rest the mouse pointer over dyn and obj to see their
        // types at compile time.
        System.Console.WriteLine(dyn.GetType());
        System.Console.WriteLine(obj.GetType());
    }
}

```

workerThreads：要由线程池根据需要创建的新的最小工作程序线程数。
completionPortThreads：要由线程池根据需要创建的新的最小空闲异步 I/O 线程数。

#### 取消任务（终止线程）

被启动的线程，每个阶段都判断 .IsCancellationRequested，然后确定是否停止运行。这取决于线程的自觉性。

**这个取消，在于信号的发生和信号的捕获，任务的取消不是实时的。**

```csharp
class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;
        dyn = dyn + 3;
        //obj = obj + 3; 报错

        // Rest the mouse pointer over dyn and obj to see their
        // types at compile time.
        System.Console.WriteLine(dyn.GetType());
        System.Console.WriteLine(obj.GetType());
    }
}

```

#### Timer

  * System.Timers.Timer，它会定期触发一个事件并在一个或多个事件接收器中执行代码。

  * System.Threading.Timer，它定期在线程池线程上执行一个回调方法。

System.Threading.Timer 其中一个构造函数定义如下：

```xml
public class AGenericClass&lt;T&gt; where T : IComparable&lt;T&gt; { }

```

allback：要定时执行的方法；
state：要传递给线程的信息(参数)；
dueTime：延迟时间，避免一创建计时器，马上开始执行方法；
period：设置定时执行方法的时间间隔；

```xml
public class AGenericClass&lt;T&gt; where T : IComparable&lt;T&gt; { }

```

### 基于任务的异步模式(Task-based asynchronous pattern)

> .NET provides three patterns for performing asynchronous operations:
>
>   * 基于任务的异步模式( **TAP** , Task-based asynchronous pattern) - uses a single method to represent the initiation and completion of an asynchronous operation. ( **async** and **await** keywords )
>
>   * 基于事件的异步模式( **EAP** , Event-based Asynchronous Pattern) - 不推荐
>
>   * 异步编程模型模式( **APM** , Asynchronous Programming Model) - 不推荐
>
>

#### 多线程编程要解决的问题

  1. 传递数据和返回结果
传递数据倒是没啥问题，只是难以获取到线程的返回值，处理线程的异常也需要技巧。

  2. 监控线程的状态
新建新的线程后，如果需要确定新线程在何时完成，需要自旋或阻塞等方式等待。

  3. 线程安全
设计时要考虑如果避免死锁、合理使用各种同步锁，要考虑原子操作，同步信号的处理需要技巧。

  4. 性能
玩多线程，最大需求就是提升性能，但是多线程中有很多坑，使用不当反而影响性能。

#### 创建&启动任务

* * *

new Task() & Start()启动 - 启动 Task，并将它安排到当前的 TaskScheduler 中执行。

```xml
public class AGenericClass&lt;T&gt; where T : IComparable&lt;T&gt; { }

```

* * *

Task.Factory.StartNew()

```xml
public class AGenericClass&lt;T&gt; where T : IComparable&lt;T&gt; { }

```

* * *

Task.Run()

```xml
public class AGenericClass&lt;T&gt; where T : IComparable&lt;T&gt; { }

```

当需要对长时间运行、计算限制的任务(计算密集型)进行精细控制时才使用 StartNew() 方法；
官方推荐使用 Task.Run 方法启动计算限制任务。
Task.Factory.StartNew() 可以实现比 Task.Run() 更细粒度的控制。

#### 取消任务

```xml
public class AGenericClass&lt;T&gt; where T : IComparable&lt;T&gt; { }

```

#### TaskCreationOptions

枚举 | Value | 说明
---|---|---
AttachedToParent | 4 | 指定将任务附加到任务层次结构中的某个父级。
DenyChildAttach | 8 | 指定任何尝试作为附加的子任务执行的子任务都无法附加到父任务，会改成作为分离的子任务执行。
HideScheduler | 16 | 防止环境计划程序被视为已创建任务的当前计划程序。
LongRunning | 2 | 指定任务将是长时间运行的、粗粒度的操作，涉及比细化的系统更少、更大的组件。
None | 0 | 指定应使用默认行为。
PreferFairness | 1 | 提示 TaskScheduler 以一种尽可能公平的方式安排任务。
RunContinuationsAsynchronously | 64 | 强制异步执行添加到当前任务的延续任务。

##### 父子任务

父任务如果先执行完毕，那么必须等待子任务完成后，父任务才算完成。

```xml
public class AGenericClass&lt;T&gt; where T : IComparable&lt;T&gt; { }

```

#### 任务异常与状态判断

Task类中描述任务字段的状态如下:

  * IsCanceled

  * IsCompleted

  * IsCompletedSuccessfully

  * IsFaulted 获取 Task是否由于未经处理异常的原因而完成。

  * Status

```xml
public class AGenericClass&lt;T&gt; where T : IComparable&lt;T&gt; { }

```

![Alt text](/media/ef20152c4a4ff065b5d0.png)

#### WhenAll 与 异步并行

```xml
public class AGenericClass&lt;T&gt; where T : IComparable&lt;T&gt; { }

```

#### WhenAny

Task.WhenAll() 当所有任务都完成时，才算完成，而 Task.WhenAny() 只要其中一个任务完成，都算完成。

```xml
public class AGenericClass&lt;T&gt; where T : IComparable&lt;T&gt; { }

```

#### WaitAll 与 同步

```xml
public class UsingEnum&lt;T&gt; where T : System.Enum { }
public class UsingDelegate&lt;T&gt; where T : System.Delegate { }
public class Multicaster&lt;T&gt; where T : System.MulticastDelegate { }

```

Task.WaitAll() 其中一个重载方法的定义:

```xml
public class UsingEnum&lt;T&gt; where T : System.Enum { }
public class UsingDelegate&lt;T&gt; where T : System.Delegate { }
public class Multicaster&lt;T&gt; where T : System.MulticastDelegate { }

```

  * millisecondsTimeout ：Int32
等待的毫秒数，-1 表示无限期等待。

  * cancellationToken ：CancellationToken
等待任务完成期间要观察的 CancellationToken。

#### 延续任务

##### ContinueWith

![Alt text](/media/80793f1dc1baa0df9fda.png)

```xml
public class UsingEnum&lt;T&gt; where T : System.Enum { }
public class UsingDelegate&lt;T&gt; where T : System.Delegate { }
public class Multicaster&lt;T&gt; where T : System.MulticastDelegate { }

```

##### TaskAwaiter

等待异步任务完成的对象并为结果提供参数

```xml
public class UsingEnum&lt;T&gt; where T : System.Enum { }
public class UsingDelegate&lt;T&gt; where T : System.Delegate { }
public class Multicaster&lt;T&gt; where T : System.MulticastDelegate { }

```

#### async & await 与同步、异步

  1. async 告诉机器，调用这个函数，不需要等他执行完，即异步执行

  2. await 用来标记一个task，告诉机器，执行到这行代码，需要等这个task完成。

```xml
public class UsingEnum&lt;T&gt; where T : System.Enum { }
public class UsingDelegate&lt;T&gt; where T : System.Delegate { }
public class Multicaster&lt;T&gt; where T : System.MulticastDelegate { }

```

### 实现一个既能同步又能异步执行的类

[《C#多线程(15)：任务基础③》](https://www.cnblogs.com/whuanle/p/12802943.html)一文中，使用TaskCompletionSource类型实现了一个实现一个支持同步和异步任务的类型，帮助读者理解TaskCompletionSource类。
笔者认为，在学习知识的过程中，应当本着这样一个原则： **理解知识的本质原理，并且记忆的东西越少越好** 。这样有如下好处：

  * 不容易混淆

  * 减轻了记忆的负担

  * 更有利于理解事物的本质

而在实践的研发中，能用最简单的方式实现想要的效果是最好不过的。

实现一个既能同步又能异步执行的类对我们理解同步异步的概念和原理很有帮助，是有必要的。因此，笔者使用最基本的Task类实现了相同的功能，这样反而更容易这个类的本质。

```xml
public class UsingEnum&lt;T&gt; where T : System.Enum { }
public class UsingDelegate&lt;T&gt; where T : System.Delegate { }
public class Multicaster&lt;T&gt; where T : System.MulticastDelegate { }

```

## Ref

<https://dotnettutorials.net/lesson/multithreading-in-csharp/>
<https://en.wikipedia.org/wiki/Thread_pool>
<https://www.cnblogs.com/whuanle/category/1756558.html>
<https://blog.csdn.net/qq_33337811/article/details/72844254>
<https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/>
