有关 .NET 中的异步编程的更多信息
通常引入异步代码的场景有两种:
• I/O 密集型操作:这些操作涉及从网络或磁盘获取的资源。
• CPU 密集型操作:这些是内存中的 CPU 密集型操作。
在本节中,我们将创建一些针对每种类型的操作使用 async
和 wait
的实际示例。 无论您是等待外部进程完成还是在应用程序中执行 CPU 密集型操作,您都可以利用异步代码来提高应用程序的性能。
I/O 密集型操作
当您使用受文件或网络操作限制的 I/O 密集型代码时,您的代码应使用 async
和 wait
来等待操作完成。
执行网络和文件 I/O 的 .NET 方法是异步的,因此不需要使用 Task.Run
:
• 示例 1:让我们看一个异步方法的示例,该方法使用 ReadToEndAsync
方法读取文本文件的内容,在找到 Environment.NewLine
字符的位置拆分文本,并将数据作为 List<string>
实例返回。 文件中的每一行文本都是列表中的一个项目:
public async Task<List<string>> GetDataAsync(string filePath)
{
using var file = File.OpenText(filePath);
var data = await file.ReadToEndAsync();
return data
.Split(new[] { Environment.NewLine },StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
示例 2:I/O 密集型操作的另一个示例是文件下载。 我们将采用前面示例中的概念,但这次要分割和返回的文件托管在网络上的 Web 服务器上。 我们将使用 HttpClient
类通过await
关键字从提供的URL下载文件,然后分割并返回列表中的文本行:
public async Task<List<string>> GetOnlineDataAsync(string url)
{
var httpClient = new HttpClient();
var data = await httpClient.GetStringAsync(url);
return data
.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
这些是一些常见的 I/O 密集型操作,但是什么是 CPU 密集型操作以及它有何不同?
CPU 密集型操作
在这种情况下,您的应用程序不会等待外部进程完成。 应用程序本身正在执行需要时间才能完成的 CPU 密集型操作,并且您希望应用程序保持响应直到操作完成。
在此示例中,我们有一个接受 List<string>
实例的方法,其中列表中的每个项目都包含此 JournalEntry
类的 XML
表示形式:
[Serializable]
public class JournalEntry
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime EntryDate { get; set; }
public string EntryText { get; set; }
}
我们假设 EntryText
可能非常大,因为在日记应用程序中编写的某些用户会将数十页文本添加到单个条目中。 每个条目都以 XML 形式存储在数据库中,加载条目的应用程序具有 DeserializeEntries
方法来反序列化每个 XML 字符串并将数据作为 List<JournalEntry>
实例返回:
private List<JournalEntry> DeserializeEntries(List<string> journalData)
{
var deserializedEntries = new List<JournalEntry>();
var serializer = new XmlSerializer(typeof (JournalEntry));
foreach (var xmlEntry in journalData)
{
if (xmlEntry == null) continue;
using var reader = new StringReader(xmlEntry);
var entry = (JournalEntry)serializer.Deserialize (reader)!;
if (entry == null) continue;
deserializedEntries.Add(entry);
}
return deserializedEntries;
}
添加日记条目数月后,用户抱怨加载现有条目所需的时间。 他们希望在加载数据时开始创建新条目。
幸运的是,使用异步 .NET 代码可以在等待长时间运行的进程完成时保持应用程序的用户界面响应。 线程可以自由地执行其他工作,直到非阻塞调用完成。 通过添加名为 DeserializeJournalDataAsync
的异步方法(该方法使用等待的 Task.Run
方法调用现有方法),客户端代码可以在用户创建新日记条目时保持响应:
public async Task<List<JournalEntry>> DeserializeJournalDataAsync(List<string> journalData)
{
return await Task.Run(() => DeserializeEntries(journalData));
}
如果您使用 JSON 格式而不是 XML 格式的序列化数据,则反序列化的同步和异步方法非常相似。 这是因为 .NET 在 System.Text.Json.JsonSerializer
类中提供了 Deserialize
和 DeserializeAsync
方法。 以下是这两种方法,并突出显示了它们的差异:
public List<JournalEntry> DeserialzeJsonEntries(List<string> journalData)
{
var deserializedEntries = new List<JournalEntry>();
foreach (var jsonEntry in journalData)
{
if (string.IsNullOrWhiteSpace(jsonEntry)) continue;
deserializedEntries.Add(JsonSerializer.Deserialize<JournalEntry>(jsonEntry)!);
}
return deserializedEntries;
}
public async Task<List<JournalEntry>> DeserializeJsonEntriesAsync(List<string> journalData)
{
var deserializedEntries = new List<JournalEntry>();
foreach (var jsonEntry in journalData)
{
if (string.IsNullOrWhiteSpace(jsonEntry)) continue;
using var stream = new MemoryStream(Encoding.Unicode.GetBytes(jsonEntry));
deserializedEntries.Add((await JsonSerializer.DeserializeAsync<JournalEntry>(stream))!);
}
return deserializedEntries;
}
Deserialize
方法接受字符串,但 DeserializeAsync
不接受。 相反,我们必须从 jsonEntry
字符串创建一个 MemoryStream
实例以传递给 DeserializeAsync
。除此之外,只有方法的返回类型不同。
让我们通过查看另一种处理日记条目列表的 JSON 反序列化的方法来结束本节。在此示例中,反序列化数据的方法仅处理单个 JSON 条目。 名为 GetJournalEntriesAsync
的父方法使用 LINQ Select
运算符为列表中的每个字符串调用 DeserializeJsonEntryAsync
,并将 IEnumerable<Task<JournalEntry>>
实例存储在 getJournalTasks
变量中:
public async Task<List<JournalEntry>> GetJournalEntriesAsync(List<string> journalData)
{
var journalTasks = journalData.Select(entry => DeserializeJsonEntryAsync(entry));
return (await Task.WhenAll(journalTasks)).ToList();
}
private async Task<JournalEntry> DeserializeJsonEntryAsync(string jsonEntry)
{
if (string.IsNullOrWhiteSpace(jsonEntry))
return new JournalEntry();
using var stream = new MemoryStream(Encoding.Unicode.GetBytes(jsonEntry));
return (await JsonSerializer.DeserializeAsync<JournalEntry>(stream))!;
}
突出显示的代码等待journalTasks
中的所有Task
对象,以JournalEntry
对象数组的形式返回每次调用的结果。 您可以使用返回类型 Task<JournalEntries[]>
声明 GetJournalEntriesAsync
,也可以使用 ToList
(如本示例中所示)返回 Task<List<JournalEntry>>
。 您可以看到当需要迭代项目列表并对每个项目进行异步调用时,LINQ 如何简化您的代码。
嵌套异步方法
当谈到使用异步方法时,当您想要保留执行顺序时,使用await
很重要。 保留对当前线程入口点的等待调用链也很重要。
例如,如果您的应用程序是控制台应用程序,则主要入口点是 Program.cs
中的 Main
方法。 如果无法使该 Main
方法异步,则 Main 下的所有方法调用都不会使用 wait
关键字进行。 这就是 .NET 现在支持异步 Main 方法的原因。 现在,当您使用 .NET 6 创建新的控制台应用程序时,它默认具有异步 Main
方法。
如果执行的入口点是事件处理程序,则应将事件处理程序方法标记为异步。 这是您唯一一次看到具有 void 返回类型的异步方法:
private async void saveButton_Click(object sender,EventArgs e)
{
await SaveData();
}
让我们看一下在控制台应用程序中链接多个嵌套异步方法的正确方法的示例:
- 首先创建一个新的控制台应用程序。 在名为
AsyncSamples
的文件夹中,运行以下命令:
dotnet new console –framework net6.0
- 该过程完成后,在 Visual Studio Code 或您选择的编辑器中打开新的
AsyncSamples.csproj
。 - 在项目中添加一个名为
TaskSample
的新类 - 在
TaskSample
类中添加以下代码:
public async Task DoThingsAsync()
{
Console.WriteLine($"Doing things in{nameof(DoThingsAsync)}");
await DoFirstThingAsync();
await DoSecondThingAsync();
Console.WriteLine($"Did things in{nameof(DoThingsAsync)}");
}
private async Task DoFirstThingAsync()
{
Console.WriteLine($"Doing something in{nameof(DoFirstThingAsync)}");
await DoAnotherThingAsync();
Console.WriteLine($"Did something in{nameof(DoFirstThingAsync)}");
}
private async Task DoSecondThingAsync()
{
Console.WriteLine($"Doing something in {nameof(DoSecondThingAsync)}");
await Task.Delay(500);
Console.WriteLine($"Did something in{nameof(DoSecondThingAsync)}");
}
private async Task DoAnotherThingAsync()
{
Console.WriteLine($"Doing something in {nameof(DoAnotherThingAsync)}");
await Task.Delay(1500);
Console.WriteLine($"Did something in{nameof(DoAnotherThingAsync)}");
}
- 现在打开
Program.cs
并添加一些代码来调用DoThingsAsync
:
using AsyncSamples;
Console.WriteLine("Start processing"…");
var taskSample = new TaskSample();
await taskSample.DoThingsAsync();
Console.WriteLie("Done processing"..");
让我们说明一下我们的项目调用的方法的顺序和层次结构。 Main
方法调用 DoThingsAsync
,DoThingsAsync
又调用 DoFirstThingAsync
和 DoSecondThingAsync
。 最后,在 DoFirstThingAsync
中调用 DoAnotherThingAsync
。 当使用await
运算符调用每个异步方法时,操作的顺序是可预测的:
图 5.1:等待方法的操作顺序
- 运行程序并检查控制台输出的顺序。 一切都应该按照预期的顺序执行:
图 5.2:检查
AsyncSamples
控制台应用程序的输出
- 接下来,我们将向
TaskSample
类添加两个附加方法:
public async Task DoingThingsWrongAsync()
{
Console.WriteLine($"Doing things in{nameof(DoingThingsWrongAsync)}");
DoFirstThingAsync();
await DoSecondThingAsync();
Console.WriteLine($"Did things in{nameof(DoingThingsWrongAsync)}");
}
public async Task DoBlockingThingsAsync()
{
Console.WriteLine($"Doing things in{nameof(DoBlockingThingsAsync)}");
DoFirstThingAsync().Wait();
await DoSecondThingAsync();
Console.WriteLine($"Did things in{nameof(DoBlockingThingsAsync)}");
}
DoingThingsWrongAsync
方法已从对 DoFirstThingAsync
的调用中删除了等待。 因此,DoSecondThingAsync
的执行将在 DoFirstThingAsync
完成之前开始。 如果后续代码都不依赖于 DoFirstThingAsync
中发生的处理,那么这可能没问题。 但是,方法内未等待的任何未处理的异常不会自动冒泡到调用方法。 调用的Task
实例的Status
值为Faulted
,IsFaulted
属性将为true
,并且Exception
属性将包含未处理的异常信息。
在上述情况下,DoFirstThingAsync
中任何未处理的异常都不会被检测到。 如果您不等待任务实例,请务必监视任务实例的状态,以防出现异常。 这是您永远不应该使用 async void
方法的原因之一。 它不返回要等待的任务实例。
DoBlockingThings
方法将保持正确的操作顺序,但通过调用 DoFirstThingAsync().Wait()
而不是等待调用,执行 DoBlockingThings
的线程将被阻塞。 它将等待对 DoFirstThingAsync
的调用完成,而不是在长时间运行的异步方法完成之前可以自由地处理其他工作。 使用诸如 Wait()
或 Result
之类的阻塞调用会快速耗尽 ThreadPool
中的可用线程。
- 更新
Program.cs
以调用所有三个公共TaskSample
方法:
using AsyncSamples;
Console.WriteLine("Start processing...");
var taskSample = new TaskSample();
await taskSample.DoThingsAsync();
Console.WriteLine("Continue processing...");
await taskSample.DoingThingsWrongAsync();
Console.WriteLine("Continue processing...");
await taskSample.DoBlockingThingsAsync();
Console.WriteLine("Done processing...");
- 现在运行程序并检查控制台输出,看看在
DoingThingsWrongAsync
中省略wait
会产生怎样的影响:
图 5.3:调用所有 TaskSample 方法时的控制台输出
每次的输出可能会略有不同,具体取决于 ThreadPool
线程的分配方式。 在这种情况下,对 DoFirstThingAsync
的第二次调用保持不完整,直到对同一方法的第三次调用开始。 尽管 Program.cs
等待对 DoingThingsWrongAsync
的调用,但在调用下一次对 DoBlockingThingsAsync
的调用后,该方法内部的代码仍在执行。
使用任务对象
在向现有项目引入线程时,直接使用 Task
对象非常有用。 正如我们在上一节中看到的,在引入 async
和 wait
时更新整个调用堆栈非常重要。 在大型代码库上,这些更改可能会很广泛,并且需要大量的回归测试。
您可以改为使用 Task
和 Task<TResult>
来包装要异步运行的现有方法。 两种任务类型都表示方法或操作正在完成的异步工作。 当方法本来会返回 void
时,可以使用 Task
。 将 Task<TResult>
与具有非 void
返回类型的方法结合使用。
以下是两个同步方法签名及其异步等效方法的示例:
public interface IAsyncExamples
{
void ProcessOrders(List<Order> orders);
Task ProcessOrdersAsync(List<Order> orders);
List<Order> GetOrders(int customerId);
Task<List<Order>> GetOrdersAsync(int customerId);
}
探索任务方法
首先,我们将在实际示例中发现一些常用的 Task
方法。 考虑一下 ProcessOrders
方法,它接受要处理和提交的订单列表。 使用的四种Task
方法如下:
• Task.Run
:在线程池上的线程上运行方法
• Task.Factory.StartNew
:在线程池上的线程上运行方法,并提供TaskCreationOptions
• processOrdersTask.ContinueWith
:当processOrdersTask
完成时,它将执行同一线程池线程上提供的方法。
• Task.WaitAll
:此方法将阻塞当前线程并等待数组中的所有任务。
这些方法已在以下代码中突出显示:
public void ProcessOrders(List<Order> orders, int customerId)
{
Task<List<Order>> processOrdersTask = Task.Run(() => PrepareOrders(orders));
Task labelTask = Task.Factory.StartNew(() => CreateLabels(orders), TaskCreationOptions.LongRunning);
Task sendTask = processOrdersTask.ContinueWith(task =>SendOrders(task.Result));
Task.WaitAll(new[] { labelTask, sendTask });
SendConfirmation(customerId);
}
这就是前面示例中每一行所发生的情况:
Task.Run
将创建一个新的后台线程并将其放入ThreadPool
中排队Task.Factory.StartNew
还将创建一个新的后台线程并将其在ThreadPool
上排队。 此外,我们提供TaskCretionOptions.LongRunning
作为StartNew
的参数,以指示创建其他线程是有必要的,因为此任务可能需要一段时间才能完成。 这将防止线程池上排队的其他任务出现延迟。ContinueWith
将在ThreadPool
线程上对SendOrders
进行排队,但该线程在processOrdersTask
完成之前不会启动。Task.WaitAll
是异步方法Task.WhenAll
的同步等效项。它将阻塞当前线程,直到labelTask
和sendTask
完成。- 最后,调用
SendConfirmation
来通知客户他们的订单已被处理并发送。
以这种方式使用任务可以获得与等待任务实现并行处理的异步方法相同的结果。 主要区别在于,当调用 WaitAll
时,当前线程将在第 4 步被阻塞。
接下来我们将探讨的另一个有用的方法是 RunSynchronously
。 这将启动一个任务,但在当前线程上同步执行它。 异步等效方法是对任务调用 Start
。
在此示例中,ProcessData
方法接受一个参数,指示是否必须在 UI 线程上处理数据。 某些数据处理可能需要与 UI 交互以向用户提供一些选项或其他反馈:
public void ProcessData(object data, bool uiRequired)
{
Task processTask = new(() => DoDataProcessing(data));
if (uiRequired)
{
// Run on current thread (UI thread assumed for example)
processTask.RunSynchronously();
}
else
{
// Run on ThreadPool thread in background
processTask.Start();
}
}
探索任务属性
在本节中,我们将回顾任务对象上可用的属性。 大多数属性都与任务的状态相关,因此我们将从 Status
属性开始。 Status
属性返回 TaskStatus
,它是一个具有八个可能值的枚举:
• Created
(0):任务已创建并初始化,但尚未在ThreadPool
上调度。
• WaitingForActivation
(1):任务正在等待 .NET 调度
• WaitingToRun
(2):任务已安排但尚未开始执行
• Running
(3):任务当前正在运行。
• WaitingForChildrenToComplete
(4):任务已完成,但附加的子任务仍在运行或等待运行
• RanToCompletion
(5):任务成功运行完成
• Canceled
(6):任务已取消并确认取消
• Faulted
(7):执行任务时遇到未处理的异常
Task
和 Task<TResult>
的以下属性是检查状态的快捷方式:
• IsCanceled
:如果任务的状态为Canceled
,则返回 true
• IsCompleted
:如果任务的状态为 RanToCompletion
、Canceled
或Faulted
,则返回 true
• IsCompletedSuccessively
:如果任务的 Status
为 RanToCompletion
,则返回 true
• IsFaulted
:如果任务的状态为Faulted,则返回 true
使用这些属性可以简化代码中的状态检查。 Task
对象的其余实例属性如下:
• AsyncState
:返回创建任务时提供的状态。 如果未提供状态,则此属性返回 null
• CreationOptions
:返回创建任务时提供的CreationOptions
值。 如果未提供选项,则默认为 TaskCreationOptions.None
。
• Exception
:返回一个AggregateException
实例,其中包含任务运行时遇到的未处理的异常。 应在处理 AggregateException
类型的 try/catch
块中调用 Wait
或 WaitAll
。
• Id
:系统为任务分配的标识符
让我们快速了解一下如何正确捕获 AggregateException
实例并检查出错任务的 Exception
属性:
Task ordersTask = Task.Run(() => ProcessOrders(orders,123));
try
{
ordersTask.Wait();
Console.WriteLine($"ordersTask Status: {ordersTask.Status}");
}
catch (AggregateException)
{
Console.WriteLine($"Exception in ordersTask! Error message: {ordersTask.Exception.Message}");
}
此代码将在完成后将任务的状态写入控制台。 如果遇到未处理的异常,错误消息将在 catch
块中写入控制台。
现在您已经更熟悉 Task
和 Task<TResult>
的成员了,让我们讨论一些从异步代码调用同步代码的用例,反之亦然。
与同步代码的互操作
当处理现有项目并向系统引入异步代码时,同步和异步代码会存在交叉点。 我们已经在本章中看到了如何处理此互操作的一些示例。 在本节中,我们将重点关注两个方向的互操作:同步调用异步和异步调用同步。
我们将创建一个示例项目,其中的类包含表示遗留代码的同步方法,以及另一组具有现代异步方法的类。
从同步方法执行异步
在此示例中,我们将使用一个 .NET 控制台应用程序来获取患者及其药物列表。 应用程序将调用同步 GetPatientAndMedicates
方法,该方法又调用异步 GetPatientInfoAsync
方法:
- 首先创建一个新的 .NET 控制台应用程序
- 将
Patient
、Provider
和Memination
类添加到Models
文件夹,并将HealthcareService
和MeminationLoader
类添加到SyncToAsync
文件夹:
图 5.4:从同步代码调用异步的初始项目结构
- 为模型类添加必要的属性:
public class Medication
{
public int Id { get; set; }
public string? Name { get; set; }
}
public class Provider
{
public int Id { get; set; }
public string? Name { get; set; }
}
public class Patient
{
public int Id { get; set; }
public string? Name { get; set; }
public List<Medication>? Medications { get; set; }
public Provider? PrimaryCareProvider { get; set; }
}
- 在
HealthcareService
类中创建GetPatientInfoAsync
方法。此方法在注入 2 秒异步延迟后创建一个具有提供者和两种药物的患者:
public async Task<Patient> GetPatientInfoAsync(int patientId)
{
await Task.Delay(2000);
Patient patient = new()
{
Id = patientId,
Name = "Smith, Terry",
PrimaryCareProvider = new Provider
{
Id = 999,
Name = "Dr. Amy Ng"
},
Medications = new List<Medication>
{
new Medication { Id = 1, Name ="acetaminophen" },
new Medication { Id = 2, Name ="hydrocortisone cream" }
}
};
return patient;
}
- 添加
MemedicationLoader
服务的实现:
public class MedicationLoader
{
private HealthcareService _healthcareService;
public MedicationLoader()
{
_healthcareService = new HealthcareService();
}
public Patient? GetPatientAndMedications(int patientId)
{
Patient? patient = null;
try
{
patient = _healthcareService.GetPatientInfoAsync(patientId).Result;
}
catch (AggregateException ae)
{
Console.WriteLine($"Error loading patient.Message: {ae.Flatten().Message}");
}
if (patient != null)
{
patient = ProcessPatientInfo(patient);
return patient;
}
else
{
return null;
}
}
private Patient ProcessPatientInfo(Patient patient)
{
// Add additional processing here.
return patient;
}
}
GetPatientAndMedicates
方法调用 GetPatientInfoAsync
并使用 Result
属性同步等待异步方法完成并返回值。 使用 Result
与在不返回值的异步方法上使用 Wait()
方法相同。 当前线程将被阻塞,直到该方法完成。
我们已将调用包装在处理 AggregateException
实例的 try/catch
块中。 如果调用成功,并且患者变量不为 null
,则在将患者数据返回给调用者之前调用 ProcessPatientInfo
。
- 将以下代码添加到
Program.cs
以调用同步方法:
using SyncAndAsyncSamples.Models;
using SyncAndAsyncSamples.SyncToAsync;
Console.WriteLine("Hello, sync to async world!");
var medLoader = new MedicationLoader();
Patient? patient = medLoader.GetPatientAndMedications(123);
Console.WriteLine($"Loaded patient: {patient.Name}with {patient.Medications.Count} medications.");
- 运行程序。 您应该在窗口中看到以下输出:
Hello, sync to async world!
Loaded patient: Smith, Terry with 2 medications.
将同步代码作为异步执行
在此示例中,我们将镜像前面的示例。 将有一个具有异步方法的 PatientLoader
实例,它使用同步方法调用 PatientService
实例:
-
将
PatientService
类添加到项目中的新AsyncToSync
文件夹中。 -
创建一个
GetPatientInfo
方法,其实现与上一示例中的GetPatientInfoAsync
方法类似:
public Patient GetPatientInfo(int patientId)
{
Thread.Sleep(2000);
Patient patient = new()
{
Id = patientId,
Name = "Smith, Terry",
PrimaryCareProvider = new Provider
{
Id = 999,
Name = "Dr. Amy Ng"
},
Medications = new List<Medication>
{
new Medication { Id = 1, Name = "acetaminophen" },
new Medication { Id = 2, Name = "hydrocortisone cream" }
}
};
return patient;
}
这里的区别在于该方法不是异步的,它返回一个 Patient
实例而不是 Task<Patient>
实例,并且我们使用 Thread.Sleep
而不是 Task.Delay
注入延迟。
3. 在 AsyncToSync
文件夹中创建 PatientLoader
类,并通过创建 PatientService
的新实例来开始其实现:
private PatientService _patientService = new PatientService();
- 现在根据前面的示例创建
ProcessPatientInfo
的异步版本:
private async Task<Patient> ProcessPatientInfoAsync(Patient patient)
{
await Task.Delay(100);
// Add additional processing here.
return patient;
}
- 现在创建
GetPatientAndMedsAsync
方法:
public async Task<Patient?> GetPatientAndMedsAsync(int patientId)
{
Patient? patient = null;
try
{
patient = await Task.Run(() => _patientService.GetPatientInfo(patientId));
}
catch (Exception e)
{
Console.WriteLine($"Error loading patient.Message: {e.Message}");
}
if (patient != null)
{
patient = await ProcessPatientInfoAsync (patient);
return patient;
}
else
{
return null;
}
}
突出显示了与上一个示例的主要区别。 GetPatientInfo
的同步类包装在对await Task.Run
的调用中,它将等待调用,而不会阻止当前线程执行其他工作。
我们现在在 catch
块中使用 Exception
而不是 AggregateException
。您应该始终将 AggregateException
与阻塞 Wait
和 Result
调用一起使用,并将 Exception
与 async
和await
一起使用。
最后,如果患者变量不为空,则等待对 ProcessPatientInfoAsync
的异步调用。
- 接下来更新
Program.cs
以调用新的PatientLoader
代码:
using SyncAndAsyncSamples.AsyncToSync;
using SyncAndAsyncSamples.Models;
Console.WriteLine("Hello, async to sync world!");
var loader = new PatientLoader();
Patient? patient = await loader.GetPatientAndMedsAsync(123);
Console.WriteLine($"Loaded patient: {patient.Name}with {patient.Medications.Count} medications.");
- 运行程序,输出应与前面的示例类似:
Hello, async to sync world!
Loaded patient: Smith, Terry with 2 medications.
处理多个后台任务
在本节中,我们将看到并行从多个源加载数据的代码示例,而不是等到方法准备好将数据返回给调用者。 同步和异步代码的技术略有不同,但总体思路是相同的。
首先,回顾一下这个调用三个异步方法并使用 Task.WhenAll
在返回患者数据之前等待的方法:
public async Task<Patient> LoadPatientAsync(int patientId)
{
var taskList = new List<Task>
{
LoadPatientInfoAsync(patientId),
LoadProviderAsync(patientId),
LoadMedicationsAsync(patientId)
};
await Task.WhenAll(taskList.ToArray());
_patient.Medications = _medications;
_patient.PrimaryCareProvider = _provider;
return _patient;
}
现在,查看该方法的同步版本,它使用 Task.WaitAll
:
public Patient LoadPatient(int patientId)
{
var taskList = new List<Task>
{
LoadPatientInfoAsync(patientId),
LoadProviderAsync(patientId),
LoadMedicationsAsync(patientId)
};
Task.WaitAll(taskList.ToArray());
_patient.Medications = _medications;
_patient.PrimaryCareProvider = _provider;
return _patient;
}
即使这个版本的代码使用阻塞的 WaitAll
调用,其执行速度也会比对这三个方法进行单独的同步调用更快。
本章的 GitHub 存储库中提供了此 ParallelPatientLoader
类的完整实现。 让我们列出一些使用 async、await 和 Task 对象的最佳实践来结束本章。
异步编程最佳实践
使用异步代码时,您应该了解许多最佳实践。 在本节中,我们将列出在日常开发中需要记住的最重要的内容。
David Fowler 是 Microsoft ASP.NET 团队的资深成员,也是 .NET 专家,他维护着一个包含许多其他最佳实践的开源列表。 我建议将此页面添加为书签,以便以后在处理您自己的项目时参考:https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#asynchronous-programming。
这些是我在使用异步代码时应遵循的首要建议(排名不分先后):
-
始终优先选择异步和等待,而不是同步方法和阻塞调用,例如
Wait()
和Result
。 如果您正在创建一个新项目,那么您应该从一开始就考虑到异步。 -
除非您使用
Task.WhenAll
同时等待多个操作,否则您应该直接await一个方法,而不是创建一个Task
实例并等待它。 -
不要使用
async void
。 您的异步方法应始终返回 Task、Task、ValueTask 或 ValueTask<TResult>
。 唯一的例外是具有返回 void 的现有签名的事件处理程序。 Event Main 方法在 .NET 6 中可以是异步的。 -
不要混合阻塞代码和异步代码。 通过调用堆栈使用异步调用。
-
使用
Task.Run
而不是Task.Factory.StartNew
除非您需要将其他参数传递给StartNew
重载方法之一。 -
长时间运行的异步方法应该支持取消。 我们将在第 11 章深入讨论取消。
-
同步共享数据的使用。 您的代码应该添加锁以防止跨线程使用的对象中的数据被覆盖。
-
始终对 I/O 密集型工作(例如网络和文件访问)使用 async 和 wait。
-
创建异步方法时,将 Async 后缀添加到其名称中。 这有助于一目了然地区分同步和异步方法。 返回用户信息的异步方法应命名为
GetUserInfoAsync
,而不是GetUserInfo
。 -
不要在异步方法中使用
Thread.Sleep
。 如果您的代码必须等待固定时间,请使用await Task.Delay
。
总结
在本章中,我们涵盖了有关使用 C# 进行异步开发的大量信息。 网。 我们首先介绍了处理应用程序中 I/O 密集型和 CPU 密集型操作的一些方法。
接下来,我们创建了一些使用 Task 和 Task