C#笔记——异步编程

本文最后更新于:2024年7月19日 下午

异步编程

最重要的话说在前头

之前的内容写得太烂了所以我决定重新整理这一部分,还是先不要读这篇文章了
本篇文章主要介绍:基于任务的异步模式(TAP)
你也许会觉得异步等于多线程,毕竟它们表面上看上去都是一样的,那就是使某些操作不阻塞主线程但是异步不等于多线程,多线程只是异步的一种实现方法!多线程也不等于异步!分清异步和多线程谢谢!

线程和进程

首先来了解一下线程进程的概念与区别。
启动程序时,系统会在内存中创建一个新的进程。进程是构成运行程序的资源的集合。这些资源包括虚地址空间、文件句柄和程序运行所需的其他许多东西。
在进程内部,系统创建了一个称为线程的内核(kernel)对象,它代表了真正执行的程序。一旦进程建立,系统会在Main方法的第一行语句处开始线程的执行。
以下是关于线程的一些知识点:

  • 默认情况下,一个进程只包含一个线程,从程序的开始一直执行到结束。
  • 线程可以派生其他线程,因此在任意时刻,一个进程都可能包含不同状态的多个线程,它们执行程序的不同部分。
  • 如果一个进程拥有多个线程,它们将共享进程的资源。
  • 系统为处理器执行所调度的单元是线程,不是进程。

什么事异步

以我的话来说,就是“改变代码执行的顺序”。比如我们现在要煎蛋、煎培根,然后倒一杯果汁,如果是同步执行,那就是“先煎蛋,煎完蛋后,然后才开始煎培根,,煎完培根后,最后才开始倒果汁”;
如果改为异步执行,那么就会“在煎蛋的时候,我们把锅丢在那不管先,然后跑去拿另外一个锅煎起了培根,但是我们仍然把锅丢在那里先不管,开始直接跑去倒果汁”,这样事情的完成顺序就可能变成“倒果汁、煎蛋/培根、煎培根/蛋”了。
当然还分为单纯的异步操作和异步多线程操作(通常都是后者居多)。后面会提到。
异步方法中的代码并不会自动在新线程中执行。
这里再借用一个杨中科老师的《ASP.NET Core技术内幕与项目实战》书中的例子(非原文内容,精简过):

餐馆点餐时,服务员固定在一桌客人旁边等待顾客点餐,点完后才继续接待下一桌客人,如果客人太多就会出现服务员忙不过来的情况,这就是“同步点餐”。
如果是服务员把菜单和点菜单给客人留下后,就去招待别的客人,待这桌客人自己写完点菜单后,服务员再过来收点菜单,这种模式下,服务员可以同时服务更多客人,这就是“异步点餐”。

应用场景

一个例子是,有时候我们的程序会向其他服务器发起请求获取数据,或是从数据库获取数据。但是一般情况下这里都会耗费大量的时间来等待其他服务器的响应,而我们不希望这种情况发生,我们更希望在等待的同时可以继续执行后面的代码,并在其他服务器回复后再返回。
另一个例子是,我们在设计交互式GUI程序时,有一些操作可能需要耗费大量时间,比如按下一个按钮后计算机执行大量计算,此时如果不使用异步操作的话,将冻结我们的GUI界面,使得用户在计算机执行完这个任务前无法继续与GUI界面进行交互。
这里也正好涉及到两个概念:I/O 绑定和 CPU 绑定。在这里,前者是 I/O 绑定,后者是 CPU 绑定。

那么在这里我自己理解一下,I/O绑定不需要开多线程,CPU绑定则需要使用异步多线程
但是有一个疑问,如果是单线程的话,异步的意义在哪呢?我自己觉得是因为异步实际上是通过状态机实现的,所以即使是单线程也能够产生“程序在同时执行多个任务”的效果。

如何区别 CPU 绑定与 I/O绑定?

以下是编写代码前应考虑的两个问题:

  1. 你的代码是否会“等待”某些内容,例如数据库中的数据? 如果答案为“是”,则你的工作是 I/O 绑定。
  2. 你的代码是否要执行开销巨大的计算? 如果答案为“是”,则你的工作是 CPU 绑定。

如果你的工作为 I/O 绑定,请使用asyncawait(而不使用Task.Run)。不应使用任务并行库。(单纯异步await,即单线程异步)
如果你的工作属于 CPU 绑定,并且你重视响应能力,请使用asyncawait,但在另一个线程上使用Task.Run生成工作。如果该工作同时适用于并发和并行,还应考虑使用任务并行库。(异步多线程,用Task.Run就会开启一个后台线程

简单说说 async/await

await关键字的意思是:调用异步方法,等异步方法执行结束后再继续向下执行

async/await 特性的结构

该特性由三个部分组成:

  1. 调用方法(calling method):该方法调用异步方法,然后在异步方法执行其任务的时候继续执行(可能在相同的线程上,也可能在不同的线程上)。
  2. 异步方法(async method):该方法异步执行其工作,然后立即返回到调用方法。
  3. await 表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个await表达式,不过如果一个都不包含的话编译器会发出警告。

async/await 的一些知识

  1. 对于调用者来说,被调用方法是否修饰为async没有区别,修饰为async只是为了在方法内使用await关键字。
  2. 只要方法的返回值是Task类型,我们就可以用await关键字对其进行调用,而不用管被调用的方法是否用async修饰。
  3. 关于“线程切换”:异步调用前的线程在异步等待期间会被放回线程池,等异步等待结束之后,一个新的空闲线程会从线程池中被获取,异步等待调用后续的代码会运行在这个新的空闲线程中。

async/await 背后的原理

编译器会把async方法编译成一个类,并根据await调用把方法切分为多个状态,对async方法的调用就会被拆分为若干次对MoveNext方法的调用(状态机)。

什么事异步方法

异步方法就是:在完成其工作之前即返回到调用方法,然后在调用方法继续执行的时候完成其工作。(应该说,异步方法可以实现在完成其工作之前立即返回到调用方法并继续执行下面的代码,而不是直接用“就是”二字划等号
在语法上,异步方法具有如下特点:

  • 方法头中包含async方法修饰符。
  • 包含一个或多个await表达式,表示可以异步完成的任务。
  • 必须具备以下几种返回类型之一:
    1. void
    2. Task
    3. Task
    4. ValueTask
    5. 任何具有公开可访问的GetAwaiter方法的类型。
  • 异步方法的形参可以为任意类型、任意数量,但不能为outref参数。
  • 按照约定,异步方法的名称应该以Async为后缀
  • 除了方法以外,lambda表达式和匿名方法也可以作为异步对象。

以下是一个异步方法的例子:

1
2
3
4
5
6
7
8
9
10
async Task<int> CountCharactersAsync(int id, string site)
{
Console.WriteLine("Starting CountCharacters");
WebClient wc = new WebClient();

string result = await wc.DownloadStringTaskAsync(new Uri(site));

Console.WriteLine("CountCharacters Completed");
return result.Length;
}

异步方法是如何提升性能的呢?

以下为《ASP.NET Core技术内幕与项目实战》中的内容:

当需要等待一个异步操作的时候,这个线程就会被放回线程池;当异步调用执行结束后,程序再从线程池取出一个线程来执行后续代码,因此服务器中的每个线程都不会空等某个操作,服务器处理并发请求的能力也就提升了。

异步方法的返回类型

上面提到异步方法的返回类型必须是:voidTaskTask<T>ValueTask<T>其中之一,下面介绍一下这几种返回类型。

Task

如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,那么异步方法可以返回一个Task类型的对象。
在这种情况下,如果异步方法中包含任何return语句,则它们不能返回任何东西。

Task

如果调用方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是Task<T>
调用方法将通过读取TaskResult属性来获取这个T类型的值。

ValueTask

这是一个值类型对象,它与Task<T>类似,但用于任务结果可能已经可用的情况。
因为它是一个值对象,所以它可以放在栈上,而无须像Task<T>对象那样在堆上分配空间。

void

如果调用方法仅仅想执行异步方法,而不需要与它做任何进一步的交互时,异步方法可以返回void类型。

任何返回Task<T>类型的异步方法,其返回值必须为T类型或可以隐式转换为T的类型。

await表达式

await表达式制定了一个异步执行的任务。其语法为:await task,由await关键字和一个空闲对象(称为任务)组成。默认情况下,这个任务在当前线程上异步运行。
一个空闲对象即是一个awaitable类型的实例。awaitable类型是指包含GetAwaiter方法的类型,该方法没有参数,返回一个awaitable类型的对象。这也是为什么上文提到的异步方法返回的类型需要是那几种之一。

创建自己的Task

当然我们也可以自行编写方法,作为await表达式的任务。最简单的方式是在你的方法中使用Task.Run方法来创建一个Task

关于Task.Run,有一点很重要,即它在不同的线程上运行你的方法。

下面以Task.Run的其中一种签名Task Run(Func<TReturn> func)作为例子,以三种不同的方式来编写任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyClass
{
public int GetNum()
{
return new Random().Next(10);
}

public async Task DoWorkAsync()
{
Func<int> getNum = new Func<int>(GetNum);
int a = await Task.Run(getNum);
int b = await Task.Run(new Func<int>(GetNum));
int c = await Task.Run(() => { return new Random().Next(10); });

Console.WriteLine($"{a} {b} {c}");
}
}
internal class AsyncLearning
{
static void Main()
{
Task t = (new MyClass()).DoWorkAsync();
t.Wait();
}
}

可以通过Thread.CurrentThread.ManagedThreadId来获取当前线程的线程ID,这样就知道方法是否运行在不同的线程上了。

上面代码的运行结果为随机输出三个不大于10的非负整型数,例如:1 4 2
下面来解释一下上面的三种Task.Run写法:

  • 第一个实例(变量a)使用GetNum创建名为getNumFunc<int>委托。然后在下一行将该委托用于Task.Run方法。
  • 第二个实例(变量b)在Task.Run方法的参数列表中创建Func<int>委托。
  • 第三个实例(变量c)直接使用与Func<int>委托兼容的lambda表达式,该lambda表达式将隐式转换为该委托。

其实Task.Run有八种重载,下表列出了这八种重载:

返回类型 签名
Task Run(Action action)
Task Run(Action action, CancellationToken token)
Task<TResult> Run(Func<TResult> function)
Task<TResult> Run(Func<TResult> function, CancellationToken token)
Task Run(Func<Task> function)
Task Run(Func<Task> function, CancellationToken token)
Task<TResult> Run(Func<Task<TResult>> function)
Task<TResult> Run(Func<Task<TResult>> function, CancellationToken token)

下表是可作为Task.Run方法第一个参数的委托类型:

委托类型 签名 含义
Action void Action() 不需要参数且无返回值的方法
Func<TResult> TResult Func() 不需要参数,但返回TResult类型对象的方法
Func<Task> Task Func() 不需要参数,但返回简单Task对象的方法
Func<Task<TResult>> Task<TResult> Func() 不需要参数,但返回Task<T>类型对象的方法

取消一个异步操作

一些 .NET 异步方法允许我们请求终止执行。这里涉及到两个不同的类:CancellationTokenCancellationTokenSource
下面是一些需要注意的点:

  • CancellationToken对象包含一个任务是否应被取消的信息
  • 拥有CancellationToken对象的任务需要定期检查其令牌(token)状态。如果CancellationToken对象的IsCancellationRequested属性为true,任务需停止其操作并返回。
  • CancellationToken是不可逆的,并且只能使用一次。一旦IsCancellationRequested属性被设置为true,就不能再更改了。
  • CancellationTokenSource对象创建可分配给不同任务的CancellationToken对象。任何持有CancellationTokenSource的对象都可以调用其Cancel方法,这会将CancellationTokenIsCancellationRequested属性设置为true

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class AsyncCancelClass
{
public async Task RunAsync(CancellationToken ct)
{
if (ct.IsCancellationRequested) return;
await Task.Run(() => CycleMethod(ct), ct);
}

void CycleMethod(CancellationToken ct)
{
Console.WriteLine("Starting Method...");
for (int i = 0; i < 5; i++) {
if (ct.IsCancellationRequested) return; // 监控 CancellationToken
Thread.Sleep(1000);
Console.WriteLine($"{ i+1 } of 5 iterations completed.");
}
}
}

class AsyncLearning
{
static void Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;

AsyncCancelClass asyncCancelClass = new AsyncCancelClass();
Task t = asyncCancelClass.RunAsync(ct);

Thread.Sleep(3000);
cts.Cancel(); // 执行取消操作

t.Wait();
Console.WriteLine($"Was Cancelled: { ct.IsCancellationRequested }");
}
}

上面代码的输出结果如下:

1
2
3
4
5
Starting Method...
1 of 5 iterations completed.
2 of 5 iterations completed.
3 of 5 iterations completed.
Was Cancelled: True

需要注意的是,该过程是协同的,即调用CancellationTokenSourceCancel时,它本身并不会执行取消操作,而是会将CancellationTokenIsCancellationRequested属性设置为true包含CancellationToken的代码负责检查该属性,并判断是否需要停止执行并返回。

异常处理和await表达式

to do…

再调用方法中同步地等待任务

我们在等待单个Task对象完成其任务时,可以使用Wait()方法。但是如果我们需要等待一组Task对象的话呢?Task类提供了两个静态方法:

  • WaitAll:等待所有任务结束
  • WaitAny:等待某一个任务结束

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class WaitAsyncClass
{
public void DoRun()
{
Task t1 = Task.Run(() => { Thread.Sleep(5000); });
Task t2 = Task.Run(() => { Thread.Sleep(3000); });

// Task.WaitAll(t1, t2);
// Task.WaitAny(t1, t2);

Console.WriteLine("Task 1: {0}Finishied", t1.IsCompleted ? " " : "Not ");
Console.WriteLine("Task 2: {0}Finishied", t2.IsCompleted ? " " : "Not ");
}
}

class AsyncLearning
{
static void Main()
{
new WaitAsyncClass().DoRun();
}
}

如果既不调用WaitAll()也不调用WaitAny()的话,输出如下:

1
2
Task 1: Not Finishied
Task 2: Not Finishied

如果调用WaitAll()方法,输出如下:

1
2
Task 1:  Finishied
Task 2: Finishied

如果调用WaitAny()方法,输出如下:

1
2
Task 1: Not Finishied
Task 2: Finishied

WaitAllWaitAny分别还包含4个重载,除了完成任务之外,还允许以不同的方式继续执行,如设置超时时间或使用CancellationToken来强制执行处理的后续部分。

在异步方法中异步地等待任务

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class DelayAsyncClass
{
Stopwatch sw = new Stopwatch();

public void DoRun()
{
Console.WriteLine("Caller: Before call.");
DelayAsync();
Console.WriteLine("Caller: After call.");
}

public async void DelayAsync()
{
sw.Start();
Console.WriteLine($"Before delay: { sw.ElapsedMilliseconds }");
await Task.Delay(1000);
// Thread.Sleep(1000);
Console.WriteLine($"After delay: { sw.ElapsedMilliseconds }");
}
}

class AsyncLearning
{
static void Main()
{
new DelayAsyncClass().DoRun();
Console.Read();
}
}

使用Task.Delay方法的输出如下:

1
2
3
4
Caller: Before call.
Before delay: 0
Caller: After call.
After delay: 1006

使用Thread.Sleep方法的输出如下:

1
2
3
4
Caller: Before call.
Before delay: 0
After delay: 1006
Caller: After call.

可以看出,Task.Delay不会阻塞线程,线程可以继续处理其他工作。

异步方法的应用

await/async模式

MainWindow.xaml中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Window x:Class="WpfAwait.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfAwait"
mc:Ignorable="d"
Title="MainWindow" Height="150" Width="250">
<StackPanel>
<Button Name="btnProcess" Width="100" Click="btnProcess_Click" HorizontalAlignment="Right" Margin="10,15,10,10">Process</Button>
<Button Name="btnCancel" Width="100" Click="btnCancel_Click" HorizontalAlignment="Right" Margin="10,0">Cancel</Button>
<ProgressBar Name="progressBar" Height="20" Width="200" Margin="10" HorizontalAlignment="Right"></ProgressBar>
</StackPanel>
</Window>

MainWindow.xaml.cs中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace WpfAwait
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
CancellationTokenSource cancellationTokenSource;
CancellationToken cancellationToken;

public MainWindow()
{
InitializeComponent();
}

private async void btnProcess_Click(object sender, RoutedEventArgs e)
{
btnProcess.IsEnabled = false; // 将Process按钮设为不可用

cancellationTokenSource = new CancellationTokenSource(); // 设置新的取消标志
cancellationToken = cancellationTokenSource.Token;

int completedPercent = 0; // 百分比
for (int i = 0; i < 10; i++) // 处理事务(迫真)
{
if (cancellationToken.IsCancellationRequested) // 定期检查取消标志的值以及时停止任务
{
break;
}

try
{
await Task.Delay(500, cancellationToken); // 实际上这里不传 cancellationToken 也能正常工作
completedPercent = (i + 1) * 10;
}
catch (TaskCanceledException ex)
{
completedPercent = i * 10;
}
progressBar.Value = completedPercent; // 设置进度条的值
}

string message = cancellationToken.IsCancellationRequested ? string.Format($"Process was cancelled at {completedPercent}%.") : "Process completed normally.";
MessageBox.Show(message, "Completion Status"); // 弹窗显示相关信息

progressBar.Value = 0; // 重置相关控件状态
btnProcess.IsEnabled = true;
btnCancel.IsEnabled = true;
}

private void btnCancel_Click(object sender, RoutedEventArgs e)
{
if (!btnProcess.IsEnabled) // 如果Process按钮不可用,说明正在处理事务,此时取消按钮可正常工作
{
btnCancel.IsEnabled = false;
cancellationTokenSource.Cancel();
}
}
}
}

BackgroundWorker模式(选读)

MainWindow.xaml中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Window x:Class="SimpleWorker.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SimpleWorker"
mc:Ignorable="d"
Title="MainWindow" Height="150" Width="350">
<StackPanel>
<ProgressBar Name="progressBar" Height="20" Width="200" Margin="10"></ProgressBar>
<Button Name="btnProcess" Width="100" Click="btnProcess_Click" Margin="5">Process</Button>
<Button Name="btnCancel" Width="100" Click="btnCancel_Click" Margin="5">Cancel</Button>
</StackPanel>
</Window>

MainWindow.xaml.cs中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
using System.ComponentModel;
using System.Threading;
using System.Windows;

namespace SimpleWorker
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
BackgroundWorker bgWorker = new BackgroundWorker();

public MainWindow()
{
InitializeComponent();

// 设置BackgroundWorker属性
bgWorker.WorkerReportsProgress = true; // 允许将进度汇报给主线程
bgWorker.WorkerSupportsCancellation = true; // 允许主线程取消后台任务

// 连接BackgroundWorker对象的处理程序
bgWorker.DoWork += DoWork_Handler;
bgWorker.ProgressChanged += ProcessChanged_Handler;
bgWorker.RunWorkerCompleted += RunWorkerCompleted_Handler;
}

private void btnProcess_Click(object sender, RoutedEventArgs e)
{
if (!bgWorker.IsBusy) // 如果后台任务未启动,则启动后台任务
{
bgWorker.RunWorkerAsync();
}
}

private void btnCancel_Click(object sender, RoutedEventArgs e)
{
bgWorker.CancelAsync(); // 把CancellationPending属性设置为True
}

private void ProcessChanged_Handler(object sender, ProgressChangedEventArgs args)
{
progressBar.Value = args.ProgressPercentage; // 通过触发ProgressChanged事件,事件调用ProcessChanged_Handler方法,从而更新进度条
}

private void DoWork_Handler(object sender, DoWorkEventArgs args)
{
BackgroundWorker worker = sender as BackgroundWorker;

for (int i = 0; i < 10; i++)
{
if (worker.CancellationPending) // 定期检查CancellationPending属性,以及时取消后台任务
{
args.Cancel = true;
break;
}
else
{
worker.ReportProgress(i * 10); // 调用ReportProgress方法来触发ProgressChanged事件
Thread.Sleep(500);
}
}
}

private void RunWorkerCompleted_Handler(object sender, RunWorkerCompletedEventArgs args)
{
progressBar.Value = 0;

if (args.Cancelled)
{
MessageBox.Show("Process was cancelled.", "Process Cancelled.");
}
else
{
MessageBox.Show("Process completed normally.", "Process Completed.");
}
}
}
}

并行循环

.NET 还提供了一个任务并行库Task Parallel Library,用于并行编程,这里只简单介绍下其中的并行循环,两者都位于System.Threading.Tasks命名空间中。

Parallel.For

Parallel.For方法有 12 个重载,其中一个是:public static ParallelLoopResult.For(int fromInclusive, int toExclusive, Action body)

  • fromInclusive参数是迭代系列的第一个整数。
  • toExclusive参数是比迭代系列的最后一个索引号大 1 的整数。
  • body是接受单个输入参数的委托。
    下面是一个例子:
    1
    Parallel.For(0, 15, i => { Console.WriteLine($"The square of {i} is {i * i}"); });
    它的一个输出如下(因为是并行处理所以每次输出都可能不一样):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    The square of 0 is 0
    The square of 10 is 100
    The square of 1 is 1
    The square of 2 is 4
    The square of 3 is 9
    The square of 4 is 16
    The square of 5 is 25
    The square of 6 is 36
    The square of 8 is 64
    The square of 9 is 81
    The square of 7 is 49
    The square of 13 is 169
    The square of 12 is 144
    The square of 11 is 121
    The square of 14 is 196

Parallel.ForEach

该方法也有很多重载,这里简单地以:static ParallelLoopResult ForEach<TSource>( IEnumerable<TSource> source, Action<TSource> body)举例说明:

  • TSource是集合中对象的类型。
  • sourceTSource对象的集合。
  • body是要应用到集合中每一个元素上的Lambda表达式。
    下面是一个例子:
    1
    2
    string[] squares = new string[] { "We", "hold", "these", "truths", "to", "be", "self-evident", "that", "all", "men", "are", "created", "equal" };
    Parallel.ForEach(squares, i => Console.WriteLine($"\"{i}\" has {i.Length} letters"));```
    它的一个输出如下(因为是并行处理所以每次输出都可能不一样):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    "created" has 7 letters
    "be" has 2 letters
    "these" has 5 letters
    "hold" has 4 letters
    "We" has 2 letters
    "truths" has 6 letters
    "men" has 3 letters
    "equal" has 5 letters
    "are" has 3 letters
    "all" has 3 letters
    "that" has 4 letters
    "to" has 2 letters
    "self-evident" has 12 letters

定时器

简单介绍一下System.Threading命名空间中的Timer类,也就是定时器类,它提供了一种定期重复运行异步方法的方式。

在 .NET BCL 中还存在着好几个Timer类,比如System.Windows.Forms.TimerSystem.Timers.Timer,可以了解下。

Timer类的其中一个常用构造函数是:Timer(TimerCallback callback, object state, uint dueTime, uint period)
其中第一个参数是回调的名字,第二个参数是传给回调的对象(可以通过这个对象来传递信息),第三个参数是等待多少毫秒后第一次调用,最后一个参数是每隔多少毫秒进行一次调用。
如果想要改变已创建的Timer类对象的dueTimeperiod,可以使用Change方法。
下面是一个演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TimerLearning
{
Stopwatch sw = new Stopwatch();
int TimesCalled = 0;

TimerLearning() { sw.Start(); }

void MyCallbackMethod(object state)
{
Console.WriteLine($"{(string)state} {++TimesCalled} {sw.ElapsedMilliseconds}");
}

static void Main()
{
TimerLearning timerLearning = new TimerLearning();
Timer timer = new Timer(timerLearning.MyCallbackMethod, "Processing timer event", 2000, 1000);
Console.WriteLine("Timer started.");

Thread.Sleep(5000); // 延时 5s 后改变 timer 的 dueTime 和 period
timer.Change(1000, 2000);

Console.ReadLine();
}
}

下面是输出结果

1
2
3
4
5
6
7
8
9
Timer started.
Processing timer event 1 2024
Processing timer event 2 3000
Processing timer event 3 4001
Processing timer event 4 5001
Processing timer event 5 6015
Processing timer event 6 8014
Processing timer event 7 10014
Processing timer event 8 12014

异步编程模式(APM)(不建议看)

异步调用同步方法

使用IAsyncResult设计模式的异步操作是通过名为BeginOperationNameEndOperationName的两个方法来实现的,这两个方法分别开始和结束异步操作 OperationName。
IAsyncResult对象存储有关异步操作的信息,其中几个重要成员有:

  • AsyncState: 一个特定于应用程序的可选对象,其中包含有关异步操作的信息
  • AsyncWaitHandle: 一个WaitHandle,可用来在异步操作完成之前阻止应用程序(或者说调用方)继续往下执行
  • CompletedSynchronously 一个值,指示异步操作是否是在用于调用BeginOperationName的线程上完成,而不是在单独的ThreadPool线程上完成
  • IsCompleted: 一个值,指示异步操作是否已完成

阻止应用继续执行

在应用等待异步操作完成期间,可以通过下面两种方法来阻止应用的主线程继续往下执行:

  1. 使用AsyncWaitHandle: result.AsyncWaitHandle.WaitOne()
  2. 使用EndOperationName: Dns.EndGetHostEntry(result)

轮询异步操作的状态

使用IsCompleted属性来确定操作是否已完成:

1
2
3
4
5
6
// result as IAsyncResult
while (result.IsCompleted != true)
{
Thread.Sleep(1000);
Console.Write(".");
}

使用 AsyncCallback 委托结束异步操作

此方法可以在单独的线程中处理异步操作结果。

1
2
AsyncCallback callBack = new AsyncCallback(() => {Console.WriteLine("An asynchronous operation has finished.")});
Dns.BeginGetHostEntry(host, callBack, host);

其中,第二个参数是异步操作结束后会调用的委托(一系列方法);第三个参数则是一个用户定义对象(Object),其中包含操作的相关信息,当操作完成时,此对象会被传递给requestCallback委托。


这里有一只爱丽丝

希望本文章能够帮到您~


C#笔记——异步编程
https://map1e-g.github.io/2023/09/21/CSharp-learning-7/
作者
MaP1e-G
发布于
2023年9月21日
许可协议