首页 > 编程语言 >异步编程场景

异步编程场景

时间:2024-07-04 11:33:15浏览次数:1  
标签:异步 场景 代码 编程 绑定 Task async await

异步模型概述

异步编程的核心是 Task 和 Task<T> 对象,这两个对象对异步操作建模。 它们受关键字 async 和 await 的支持。 在大多数情况下模型十分简单:

  • 对于 I/O 绑定代码,等待一个在 async 方法中返回 Task 或 Task<T> 的操作。
  • 对于 CPU 绑定代码,等待一个使用 Task.Run 方法在后台线程启动的操作。

await 关键字有这奇妙的作用。 它控制执行 await 的方法的调用方,且它最终允许 UI 具有响应性或服务具有灵活性。

I/O 绑定示例:从 Web 服务下载数据

你可能需要在按下按钮时从 Web 服务下载某些数据,但不希望阻止 UI 线程。

s_downloadButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI as the request
    // from the web service is happening.
    //
    // The UI thread is now free to perform other work.
    var stringData = await s_httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

CPU 绑定示例:为游戏执行计算

假设你正在编写一个移动游戏,在该游戏中,按下某个按钮将会对屏幕中的许多敌人造成伤害。 执行伤害计算的开销可能极大,而且在 UI 线程中执行计算有可能使游戏在计算执行过程中暂停!

static DamageResult CalculateDamageDone()
{
    return new DamageResult()
    {
        // Code omitted:
        //
        // Does an expensive calculation and returns
        // the result of that calculation.
    };
}

s_calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI while CalculateDamageDone()
    // performs its work. The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

需了解的要点

  • 异步代码可用于 I/O 绑定和 CPU 绑定代码,但在每个方案中有所不同。
  • 异步代码使用 Task<T> 和 Task,它们是对后台所完成的工作进行建模的结构。
  • async 关键字将方法转换为异步方法,这使你能在其正文中使用 await 关键字。
  • 应用 await 关键字后,它将挂起调用方法,并将控制权返还给调用方,直到等待的任务完成。
  • 仅允许在异步方法中使用 await

识别 CPU 绑定和 I/O 绑定工作

本指南的前两个示例演示如何将 async 和 await 用于 I/O 绑定和 CPU 绑定工作。 确定所需执行的操作是 I/O 绑定或 CPU 绑定是关键,因为这会极大影响代码性能,并可能导致某些构造的误用。

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

  1. 你的代码是否会“等待”某些内容,例如数据库中的数据?

    如果答案为“是”,则你的工作是 I/O 绑定。

  2. 你的代码是否要执行开销巨大的计算?

    如果答案为“是”,则你的工作是 CPU 绑定。

如果你的工作为 I/O 绑定,请使用 async 和 await(而不使用 Task.Run)。 不应使用任务并行库。

如果你的工作属于 CPU 绑定,并且你重视响应能力,请使用 async 和 await,但在另一个线程上使用 Task.Run 生成工作。 如果该工作同时适用于并发和并行,还应考虑使用任务并行库

此外,应始终对代码的执行进行测量。 例如,你可能会遇到这样的情况:多线程处理时,上下文切换的开销高于 CPU 绑定工作的开销。 每种选择都有折衷,应根据自身情况选择正确的折衷方案。

等待多个任务完成

你可能发现自己处于需要并行检索多个数据部分的情况。 Task API 包含两种方法(即 Task.WhenAll 和 Task.WhenAny),这些方法允许你编写在多个后台作业中执行非阻止等待的异步代码。

private static async Task<User> GetUserAsync(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.

    return await Task.FromResult(new User() { id = userId });
}

private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

重要信息和建议

对于异步编程,有一些细节需要注意,以防止意外行为。

  • async 方法需要在主体中有 await 关键字,否则它们将永不暂停!

    这一点需牢记在心。 如果 await 未用在 async 方法的主体中,C# 编译器将生成一个警告,但此代码将会以类似普通方法的方式进行编译和运行。 这种方式非常低效,因为由 C# 编译器为异步方法生成的状态机将不会完成任何任务。

  • 添加“Async”作为编写的每个异步方法名称的后缀。

    这是 .NET 中的惯例,以便更为轻松地区分同步和异步方法。 未由代码显式调用的某些方法(如事件处理程序或 Web 控制器方法)并不一定适用。 由于它们未由代码显式调用,因此对其显式命名并不重要。

  • async void 应仅用于事件处理程序。

    async void 是允许异步事件处理程序工作的唯一方法,因为事件不具有返回类型(因此无法利用 Task 和 Task<T>)。 其他任何对 async void 的使用都不遵循 TAP 模型,且可能存在一定使用难度,例如:

    • async void 方法中引发的异常无法在该方法外部被捕获。
    • async void 方法很难测试。
    • async void 方法可能会导致不良副作用(如果调用方不希望方法是异步的话)。
  • 在 LINQ 表达式中使用异步 lambda 时请谨慎

    LINQ 中的 Lambda 表达式使用延迟执行,这意味着代码可能在你并不希望结束的时候停止执行。 如果编写不正确,将阻塞任务引入其中时可能很容易导致死锁。 此外,此类异步代码嵌套可能会对推断代码的执行带来更多困难。 Async 和 LINQ 的功能都十分强大,但在结合使用两者时应尽可能小心。

  • 采用非阻止方式编写等待任务的代码

    通过阻止当前线程来等待 Task 完成的方法可能导致死锁和已阻止的上下文线程,且可能需要更复杂的错误处理方法。 下表提供了关于如何以非阻止方式处理等待任务的指南:

 

  • 如果可能,请考虑使用 ValueTask

    从异步方法返回 Task 对象可能在某些路径中导致性能瓶颈。 Task 是引用类型,因此使用它意味着分配对象。 如果使用 async 修饰符声明的方法返回缓存结果或以同步方式完成,那么额外的分配在代码的性能关键部分可能要耗费相当长的时间。 如果这些分配发生在紧凑循环中,则成本会变高。 有关详细信息,请参阅通用的异步返回类型

  • 考虑使用 ConfigureAwait(false)

    常见的问题是“应何时使用 Task.ConfigureAwait(Boolean) 方法?”。 该方法允许 Task 实例配置其 awaiter。 这是一个重要的注意事项,如果设置不正确,可能会影响性能,甚至造成死锁。 有关 ConfigureAwait 的详细信息,请参阅 ConfigureAwait 常见问题解答

  • 编写状态欠缺的代码

    请勿依赖全局对象的状态或某些方法的执行。 请仅依赖方法的返回值。 为什么?

    • 这样更容易推断代码。
    • 这样更容易测试代码。
    • 混合异步和同步代码更简单。
    • 通常可完全避免争用条件。
    • 通过依赖返回值,协调异步代码可变得简单。
    • (好处)它非常适用于依赖关系注入。

建议的目标是实现代码中完整或接近完整的引用透明度。 这么做能获得可预测、可测试和可维护的代码库。

 

 

  

 

标签:异步,场景,代码,编程,绑定,Task,async,await
From: https://www.cnblogs.com/friend/p/18283285

相关文章

  • Java多线程编程
    1.进程进程是指操作系统中正在运行的程序实例,它是系统资源分配的基本单位。每个进程都拥有独立的内存空间和系统资源,可以看作是程序的一次执行过程。2.线程线程是进程中的执行单元,也被称为轻量级进程(LightWeightProcess)。一个进程可以包含多个线程,这些线程共享进......
  • 高并发场景下的热点key问题探析与应对策略
    目录一、问题描述二、发现机制三、解决策略分析 (一)解决策略一:多级缓存策略客户端本地缓存代理节点本地缓存 (二)解决策略二:多副本策略 (三)解决策略三:热点Key拆分与动态分散四、总结干货分享,感谢您的阅读!在高并发场景下,缓存作为前置查询机制,显著减轻了数据库的压......
  • 【Python函数编程实战】:从基础到进阶,打造代码复用利器
    文章目录......
  • 函数式编程和命令式编程
    函数式编程(FunctionalProgramming,FP)与命令式编程(ImperativeProgramming,IP)是编程领域中两大截然不同的范式,它们在解决问题的策略和哲学上存在着本质的差异。函数式编程(FunctionalProgramming)函数式编程核心理念在于通过函数来定义、封装及组合计算逻辑。其核心特性之一......
  • springboot中事务失效的一些场景以及如何应对
    @Transactional是基于AOP的,因此事务发生需要两个条件:1.添加@Transactional注解2.使用代理对象 失效场景:同一个类中直接调用的类方法,而被调方法带有@Transactional下面这段代码会抛出runtimeException异常,但是事务不会回滚。即insert生效了因为b()在调用a()方法时,对象已经不......
  • 《智能计算系统》第五章 编程框架原理(上)课程笔记
    《智能计算系统》第五章编程框架原理(上)课程视频链接:https://www.bilibili.com/video/BV1Ei421i7Rg本文源自于B站国科大计算所智能计算系统课程官方账号所公开上传的视频,在原有视频之上,提取了关键帧、将音频转成了文字并进行了校正,以便学习使用。在此,也感谢国科大计算所智能......
  • Java 网络编程
    IP地址IP地址的组成IP地址=网络地址+主机地址网络地址:标识计算机或网络设备所在的网段主机地址:标识特定主机或网络设备一般是C类4组8位2进制组成cmd查看自己的IP:ipconfigcmd查看网络是否可以链接:pingIP地址五层协议1、物理层:基于电气特性的高低电压(电信号)高......
  • 并发编程 - 第三章
    线程基础机制1.1守护线程Daemon守护线程可以简单地理解为后台运行线程。进程结束,守护线程自然而然地就会结束,不需要手动的去关心和通知其状态。例如:在应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。Java的垃圾回收也是一个守护线程。守护线程......
  • 【重走编程路】设计模式概述(三) -- 单例模式
    文章目录前言设计模式详解3.单例模式(Singleton)问题解决方案应用场景实现代码1.懒汉式单例模式2.加锁的懒汉式单例模式3.饿汉式单例模式4.静态内部变量(c++11)5.call_once实现懒汉单例前言创建型模式主要关注对象的创建过程,提供了一种创建对象的最佳方式,并隐......
  • Java编程从入门到放弃
    1.配置开发环境安装JDK官网下载地址:https://www.oracle.com/java/technologies/downloads/配置环境变量最新版本JDK22无需手动配置环境变量。老版本:此电脑-右键属性-高级系统设置-环境变量-系统变量-Path-编辑C:\Java\jdk1.8.0_65\bin检查结果java-versionHelloWor......