首页 > 编程语言 >并发编程-5.使用 C# 进行异步编程

并发编程-5.使用 C# 进行异步编程

时间:2023-10-14 11:35:12浏览次数:45  
标签:异步 Task patient C# 编程 调用 方法 public

有关 .NET 中的异步编程的更多信息

通常引入异步代码的场景有两种:
• I/O 密集型操作:这些操作涉及从网络或磁盘获取的资源。
• CPU 密集型操作:这些是内存中的 CPU 密集型操作。

在本节中,我们将创建一些针对每种类型的操作使用 asyncwait 的实际示例。 无论您是等待外部进程完成还是在应用程序中执行 CPU 密集型操作,您都可以利用异步代码来提高应用程序的性能。

I/O 密集型操作

当您使用受文件或网络操作限制的 I/O 密集型代码时,您的代码应使用 asyncwait 来等待操作完成。

执行网络和文件 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 类中提供了 DeserializeDeserializeAsync 方法。 以下是这两种方法,并突出显示了它们的差异:

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();
}

让我们看一下在控制台应用程序中链接多个嵌套异步方法的正确方法的示例:

  1. 首先创建一个新的控制台应用程序。 在名为 AsyncSamples 的文件夹中,运行以下命令:
dotnet new console –framework net6.0
  1. 该过程完成后,在 Visual Studio Code 或您选择的编辑器中打开新的 AsyncSamples.csproj
  2. 在项目中添加一个名为TaskSample的新类
  3. 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)}");
}
  1. 现在打开 Program.cs 并添加一些代码来调用 DoThingsAsync
using AsyncSamples;
Console.WriteLine("Start processing"…");
var taskSample = new TaskSample();
await taskSample.DoThingsAsync();
Console.WriteLie("Done processing"..");

让我们说明一下我们的项目调用的方法的顺序和层次结构。 Main 方法调用 DoThingsAsyncDoThingsAsync 又调用 DoFirstThingAsyncDoSecondThingAsync。 最后,在 DoFirstThingAsync 中调用 DoAnotherThingAsync。 当使用await运算符调用每个异步方法时,操作的顺序是可预测的:

图 5.1:等待方法的操作顺序

image

  1. 运行程序并检查控制台输出的顺序。 一切都应该按照预期的顺序执行:

图 5.2:检查 AsyncSamples 控制台应用程序的输出

image

  1. 接下来,我们将向 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 值为FaultedIsFaulted 属性将为true,并且Exception 属性将包含未处理的异常信息。

在上述情况下,DoFirstThingAsync 中任何未处理的异常都不会被检测到。 如果您不等待任务实例,请务必监视任务实例的状态,以防出现异常。 这是您永远不应该使用 async void 方法的原因之一。 它不返回要等待的任务实例。

DoBlockingThings 方法将保持正确的操作顺序,但通过调用 DoFirstThingAsync().Wait() 而不是等待调用,执行 DoBlockingThings 的线程将被阻塞。 它将等待对 DoFirstThingAsync 的调用完成,而不是在长时间运行的异步方法完成之前可以自由地处理其他工作。 使用诸如 Wait()Result 之类的阻塞调用会快速耗尽 ThreadPool 中的可用线程。

  1. 更新 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...");
  1. 现在运行程序并检查控制台输出,看看在 DoingThingsWrongAsync 中省略 wait 会产生怎样的影响:

图 5.3:调用所有 TaskSample 方法时的控制台输出

image

每次的输出可能会略有不同,具体取决于 ThreadPool 线程的分配方式。 在这种情况下,对 DoFirstThingAsync 的第二次调用保持不完整,直到对同一方法的第三次调用开始。 尽管 Program.cs 等待对 DoingThingsWrongAsync 的调用,但在调用下一次对 DoBlockingThingsAsync 的调用后,该方法内部的代码仍在执行。

使用任务对象

在向现有项目引入线程时,直接使用 Task 对象非常有用。 正如我们在上一节中看到的,在引入 asyncwait 时更新整个调用堆栈非常重要。 在大型代码库上,这些更改可能会很广泛,并且需要大量的回归测试。

您可以改为使用 TaskTask<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);
}

这就是前面示例中每一行所发生的情况:

  1. Task.Run将创建一个新的后台线程并将其放入ThreadPool中排队
  2. Task.Factory.StartNew 还将创建一个新的后台线程并将其在 ThreadPool 上排队。 此外,我们提供 TaskCretionOptions.LongRunning 作为 StartNew 的参数,以指示创建其他线程是有必要的,因为此任务可能需要一段时间才能完成。 这将防止线程池上排队的其他任务出现延迟。
  3. ContinueWith 将在 ThreadPool 线程上对 SendOrders 进行排队,但该线程在 processOrdersTask 完成之前不会启动。
  4. Task.WaitAll 是异步方法 Task.WhenAll 的同步等效项。它将阻塞当前线程,直到 labelTasksendTask 完成。
  5. 最后,调用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):执行任务时遇到未处理的异常

TaskTask<TResult> 的以下属性是检查状态的快捷方式:

IsCanceled:如果任务的状态为Canceled,则返回 true
IsCompleted:如果任务的状态为 RanToCompletionCanceledFaulted,则返回 true
IsCompletedSuccessively:如果任务的 StatusRanToCompletion,则返回 true
IsFaulted:如果任务的状态为Faulted,则返回 true

使用这些属性可以简化代码中的状态检查。 Task 对象的其余实例属性如下:

AsyncState:返回创建任务时提供的状态。 如果未提供状态,则此属性返回 null

CreationOptions:返回创建任务时提供的CreationOptions 值。 如果未提供选项,则默认为 TaskCreationOptions.None
Exception:返回一个AggregateException 实例,其中包含任务运行时遇到的未处理的异常。 应在处理 AggregateException 类型的 try/catch 块中调用 WaitWaitAll
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 块中写入控制台。
现在您已经更熟悉 TaskTask<TResult> 的成员了,让我们讨论一些从异步代码调用同步代码的用例,反之亦然。

与同步代码的互操作

当处理现有项目并向系统引入异步代码时,同步和异步代码会存在交叉点。 我们已经在本章中看到了如何处理此互操作的一些示例。 在本节中,我们将重点关注两个方向的互操作:同步调用异步和异步调用同步。

我们将创建一个示例项目,其中的类包含表示遗留代码的同步方法,以及另一组具有现代异步方法的类。

从同步方法执行异步

在此示例中,我们将使用一个 .NET 控制台应用程序来获取患者及其药物列表。 应用程序将调用同步 GetPatientAndMedicates 方法,该方法又调用异步 GetPatientInfoAsync 方法:

  1. 首先创建一个新的 .NET 控制台应用程序
  2. PatientProviderMemination 类添加到 Models 文件夹,并将 HealthcareServiceMeminationLoader 类添加到 SyncToAsync 文件夹:

图 5.4:从同步代码调用异步的初始项目结构

image

  1. 为模型类添加必要的属性:
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; }
}
  1. 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;
}
  1. 添加 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

  1. 将以下代码添加到 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.");
  1. 运行程序。 您应该在窗口中看到以下输出:
Hello, sync to async world!
Loaded patient: Smith, Terry with 2 medications.

将同步代码作为异步执行

在此示例中,我们将镜像前面的示例。 将有一个具有异步方法的 PatientLoader 实例,它使用同步方法调用 PatientService 实例:

  1. PatientService 类添加到项目中的新 AsyncToSync 文件夹中。

  2. 创建一个 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();
  1. 现在根据前面的示例创建 ProcessPatientInfo 的异步版本:
private async Task<Patient> ProcessPatientInfoAsync(Patient patient)
{
    await Task.Delay(100);
    // Add additional processing here.
    return patient;
}
  1. 现在创建 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 与阻塞 WaitResult 调用一起使用,并将 Exceptionasyncawait 一起使用。
最后,如果患者变量不为空,则等待对 ProcessPatientInfoAsync 的异步调用。

  1. 接下来更新 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.");
  1. 运行程序,输出应与前面的示例类似:
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。

这些是我在使用异步代码时应遵循的首要建议(排名不分先后):

  1. 始终优先选择异步和等待,而不是同步方法和阻塞调用,例如 Wait()Result。 如果您正在创建一个新项目,那么您应该从一开始就考虑到异步。

  2. 除非您使用Task.WhenAll同时等待多个操作,否则您应该直接await一个方法,而不是创建一个Task实例并等待它。

  3. 不要使用async void。 您的异步方法应始终返回 Task、Task、ValueTask 或 ValueTask<TResult>。 唯一的例外是具有返回 void 的现有签名的事件处理程序。 Event Main 方法在 .NET 6 中可以是异步的。

  4. 不要混合阻塞代码和异步代码。 通过调用堆栈使用异步调用。

  5. 使用Task.Run 而不是Task.Factory.StartNew 除非您需要将其他参数传递给StartNew 重载方法之一。

  6. 长时间运行的异步方法应该支持取消。 我们将在第 11 章深入讨论取消。

  7. 同步共享数据的使用。 您的代码应该添加锁以防止跨线程使用的对象中的数据被覆盖。

  8. 始终对 I/O 密集型工作(例如网络和文件访问)使用 async 和 wait。

  9. 创建异步方法时,将 Async 后缀添加到其名称中。 这有助于一目了然地区分同步和异步方法。 返回用户信息的异步方法应命名为 GetUserInfoAsync,而不是 GetUserInfo

  10. 不要在异步方法中使用Thread.Sleep。 如果您的代码必须等待固定时间,请使用await Task.Delay

总结

在本章中,我们涵盖了有关使用 C# 进行异步开发的大量信息。 网。 我们首先介绍了处理应用程序中 I/O 密集型和 CPU 密集型操作的一些方法。
接下来,我们创建了一些使用 Task 和 Task 类的实际示例,并发现了如何使用多个 Task 对象。 您获得了一些有关现代异步代码和旧同步方法之间互操作的实用建议。 最后,我们介绍了使用异步代码和任务对象时需要记住的一些最重要的规则。

标签:异步,Task,patient,C#,编程,调用,方法,public
From: https://www.cnblogs.com/King2019Blog/p/17763934.html

相关文章

  • macOS 安装 clang-tidy
    先安装homebrew,网上教程很多,推荐官方教程,此处略过通过brew安装llvmbrewinstallllvm创建软连接,指向homebrew安装的clang-tidymkdir-p/usr/local/bin/ln-s/opt/homebrew/Cellar/llvm/13.0.0_1/bin/clang-tidy/usr/local/bin/clang-tidy注1:推荐创建软连......
  • ATen/cuda/CUDAContext.h: No such file or directory缺少这个文件
    报错:(FlowGANCUDA10.0)lww@r750:~/projects/FlowGAN-main/FlowGAN-main/lib/metrics/pytorch_structural_losses$makeTraceback(mostrecentcalllast):File"<string>",line1,in<module>ModuleNotFoundError:Nomodulenamed'torch�......
  • 图文并茂手把手教你在MAC配置Android,nodejs环境,配置安卓真机支持投屏以及测试
    先说nodejs和npm这个很简单,只需要点击下面链接,安装node.js环境即可https://nodejs.org/zh-cn/AndroidAndroidStudio下载地址及版本说明Android开发者官网:https://developer.android.com/index.html(全球)https://developer.android.googl......
  • Argument for '--moduleResolution' option must be: 'node', Unknown compiler opt
    node_modules/@vue/tsconfig/tsconfig.json(12,25):errorTS6046:Argumentfor'--moduleResolution'optionmustbe:'node','classic','node16','nodenext'.node_modules/@vue/tsconfig/tsconfig.json(33,5):erro......
  • 无涯教程-Matplotlib - 刻度标签(Tick Label)
    刻度(Ticks)是表示轴上数据点的标签,到目前为止,在无涯教程之前的所有示例中,Matplotlib都自动绘制了轴上的间隔点的任务.Matplotlib的默认刻度定位器设计用于在许多常见情况下通常就足够了。xticks()和yticks()函数将列表对象作为参数,列表中的元素表示相应动作上将显示刻度的位......
  • # 如何将df_test['col']中的list对象拆分为两列, 使结果为df_result
    df_test=pd.DataFrame(data=[[[0,1]],[[1,0]]],columns=['col'])df_result=pd.DataFrame(data=[[0,1],[1,0]],columns=['col1','col2'])#如何将df_test['col&#......
  • 博学谷学习记录 自我总结 用心分享 | Docker容器化
    前言容器技术、虚拟化技术已经成为一种被大家广泛认可的服务器资源共享方式,容器技术可以在按需构建操作系统实例的过程当中为系统管理员提供极大的灵活性。由于hypervisor虚拟化技术仍然存在一些性能和资源使用效率方面的问题,因此容器技术(Container)结合虚拟化技术的解决方案正在......
  • 并发编程-4.用户界面响应能力和线程
    利用后台线程在第一章中,我们学习了如何创建后台线程并讨论了它们的一些用途。后台线程的优先级低于进程的主线程和其他线程池线程。此外,活动的后台线程不会阻止用户或系统终止应用程序。这意味着后台线程非常适合执行以下任务:•写入日志和分析数据•监控网络或文件系统资源......
  • 如何让cmake找到boost库
    title:aliases:tags:-cmake-boostcategory:-方法stars:url:creation-time:2023-10-1309:46modification-time:2023-10-1411:00:47在此之前,我们已经[[使用mingw-w64编译Boost]]。然后,我们来编写项目的CMakeList文件。定义好关键的变量:set(BOOST......
  • 使用 Docker 在 Linux 上运行 Delphi 应用程序
     从RadStudio10.2Tokyo开始,可以编译和运行Linux服务器应用程序(无用户界面)。我们将使用Ubuntu准备一个docker映像,以及通过PAServer在Linux上运行Delphi应用程序所需的一切。使用Docker,我们可以将这些应用程序在Linux容器中部署到我们的生产系统中。从建立一......