同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;

当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。

异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。

net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;

net5.0推出了async/await.

1.什么是异步?

async 和 await 是 C# 的语法糖,用来简化异步编程模型,首先来看下 async 和 await 的代码结构。

异步操作通常用于执行完成时间可能较长的任务,如打开大文件、连接远程计算机或查询数据库=异步操作在主应用程序线程以外的线程中执行。应用程序调用方法异步执行某个操作时,应用程序可在异步方法执行其任务时继续执行。

异步本质是通过线程池提高线程的利用率

  • 异步采用IO的DMA模式,不会消耗CPU资源。计算密集的工作,采用多线程。IO密集的工作,采用异步
  • 举例:网络爬虫爬数据,如果数据很庞大,这个时候就需要使用异步了。或者访问Web的时候,对资源的访问可能很慢或者有延迟,同步过程中受阻碍,就需要利用异步
  • DMA:Direct Memory Access是IO的操作模式,可以直接访问内存,不经过CPU,不消耗CPU资源。
  • 异步和多线程区别就是,充分利用DMA释放CPU压力。
  • 异步实现方式:.net中实现异步的方式有定时器多线程,一般都是使用多线程

2.同步与异步的区别

同步和异步主要用于修饰方法

同步(Synchronous)
当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法

异步(Asynchronous)
当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法

实质:异步操作,启动了新的线程,主线程与方法线程并行执行。

并发与并行

核心区别概览:

  • 并发 (Concurrency): 处理很多事情。它关注的是管理多个任务,使得这些任务在执行过程中可以交织进行(interleave)。这些任务不一定在同一时刻真正执行,可能是在单个处理核心上通过时间片轮转快速切换,给人一种同时进行的错觉。并发的重点在于管理和协调多个独立任务,以提高系统的响应性和吞吐量,特别是在存在大量等待(如 I/O 操作)的情况下。
  • 并行 (Parallelism): 同时做很多事情。它关注的是实际同时执行多个任务,这需要多个处理单元(如多核 CPU)来在同一物理时刻执行不同的指令。并行的重点在于利用多核资源来加速单个计算密集型任务的完成时间。

详细解释与比喻:

1. 并发 (Concurrency)

  • 概念: 并发是指系统具有同时处理多个任务的能力。这些任务的执行时间可以重叠,但它们不一定在同一瞬间执行。
  • 如何实现: 在单核 CPU 上,并发是通过操作系统或编程语言的调度器实现的,它会在不同的任务之间快速切换(时间片)。当一个任务在等待资源(如网络响应、磁盘读写)时,CPU 可以切换到另一个任务执行,从而不浪费 CPU 时间。在多核 CPU 上,并发的任务可以被调度到不同的核心上,从而实现真正的并行,但并发本身并不强制要求并行。
  • 目标: 提高系统的响应性(用户界面不卡顿)和吞吐量(单位时间内处理的任务总数),有效地利用资源,特别是在存在 I/O 瓶颈时。
  • 比喻:
    • 单核 CPU 上的并发: 一个厨师同时做多道菜。他可能先切一会儿菜,然后去炒一会儿肉,再去看看汤。他并没有同时做所有事情,而是在不同任务之间快速切换,给人感觉他“同时”在处理很多事情。
    • 多核 CPU 上的并发: 就像多个厨师在厨房里,他们各自做各自的菜(可能是独立的菜),但他们工作的节奏是独立的,互相之间可能会因为共用灶台或冰箱而需要协调。
  • C# 中的体现:
    • async / await 是 C# 实现异步并发的主要方式,特别适合处理 I/O 密集型任务。当一个 await 遇到一个需要等待的操作时,当前任务会暂停执行并将控制权交还给调用者,CPU 可以去做其他事情,直到等待的操作完成。这提高了程序的响应性。
    • 创建多个独立的 Task 对象并在主线程或其他线程中启动它们,而无需等待它们立即完成。

2. 并行 (Parallelism)

  • 概念: 并行是指多个任务或一个任务的多个部分在同一物理时刻在不同的处理单元上同时执行。
  • 如何实现: 这需要多核 CPU 或多处理器系统。操作系统可以将不同的线程调度到不同的核心上同时执行。
  • 目标: 缩短单个计算密集型任务的总执行时间,充分利用多核处理器的计算能力。
  • 比喻:
    • 多个厨师同时在各自的灶台上炒菜。
    • 一条多车道的高速公路,多辆车可以同时并排前进。
  • C# 中的体现:
    • System.Threading.Tasks.Parallel 类(如 Parallel.For, Parallel.ForEach):这些方法将一个循环或其他集合的处理工作分解成多个部分,并在线程池中的多个线程上并行执行这些部分。
    • PLINQ (Parallel LINQ):通过 .AsParallel() 方法,可以将 LINQ 查询并行化,使其在多个线程上执行。
    • 手动创建和管理多个 Thread 或 Task,并将计算工作分配给它们,然后在不同的核心上同时运行。

总结异同点:

特性 并发 (Concurrency) 并行 (Parallelism)
核心 处理很多事情 (Dealing with many things) 同时做很多事情 (Doing many things simultaneously)
关注点 管理和协调多个任务的交织执行 实际同时执行多个任务,利用多核加速计算
实现基础 调度和切换(单核或多核),异步等待释放资源 多处理单元(多核 CPU)
目的 提高响应性、吞吐量,有效利用资源(尤其是 I/O) 缩短计算时间,利用多核计算能力
是否需要多核 不一定(单核可实现伪并行/异步) 需要(才能实现真并行)
关系 更广泛的概念,可以包含并行 并发的一种特殊形式,需要硬件支持才能实现
C# 主要实现 async/await, Task (用于 I/O 或管理) Parallel.For, Parallel.ForEach, PLINQ (.AsParallel())

简单来说:

  • 并发 是关于如何组织代码,使其能够处理多个独立的任务,即使在单核上它们也可能交替执行。
  • 并行 是关于如何利用多核硬件,使得多个任务或任务的一部分能够真正同时执行,以提高计算速度。

并行是实现并发的一种手段,尤其是在计算密集型任务中。而并发的概念更广,它还包括异步编程,这在处理 I/O 密集型任务时非常有用,即使没有多核,也能提高效率和响应性。


3.异步和多线程的区别

异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。

异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情。实现异步可以采用多线程技术或则交给另外的进程来处理。

简单的说就是:异步线程是由线程池负责管理,而多线程,我们可以自己控制,当然在多线程中我们也可以使用线程池。
就拿网络扒虫而言,如果使用异步模式去实现,它使用线程池进行管理。异步操作执行时,会将操作丢给线程池中的某个工作线程来完成。当开始I/O操作的时候,异步会将工作线程还给线程池,这意味着获取网页的工作不会再占用任何CPU资源了。直到异步完成,即获取网页完毕,异步才会通过回调的方式通知线程池。可见,异步模式借助于线程池,极大地节约了CPU的资源。

注:DMA(Direct Memory Access)直接内存存取,顾名思义DMA功能就是让设备可以绕过处理器,直接由内存来读取资料。通过直接内存访问的数据交换几乎可以不损耗CPU的资源。在硬件中,硬盘、网卡、声卡、显卡等都有直接内存访问功能。异步编程模型就是让我们充分利用硬件的直接内存访问功能来释放CPU的压力。

在这里插入图片描述

举个通俗的例子

同步通信机制

你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。什么时候查完什么时候结束,耽误时间啊

异步通信机制

书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。不耽误事


4.Async Await

Async 和 await是代码标记,它标记代码位置为任务完成后控件应该恢复的位置

异步Task(多任务)本身就是两个不同的概念

异步是同时执行多个任务,

而Task则是允许多个任务可以在线程内同时进行多任务的同时进行,则是由异步进行,而异步方法,必须为Task

  • 同步方法

可以认为程序是按照你写这些代码时所采用的顺序执行相关的指令的。

  • 异步方法

异步是指调用一个方法A,调用后回立即返回(不用等A执行完成),接着调用方法B

可以在尚未完成所有指令的时候提前返回(如上面的洗衣服过程没执行完就返回去洗澡了),等到该方法等候的那项任务执行完毕后,在令这个方法从早前还没执行完的那个地方继续往下运行。

Demo1:

static void Main(string[] args)
{
    Task.Run(() =>          //异步开始执行
    {
        Thread.Sleep(1000);                 //异步执行一些任务
        Console.WriteLine("Hello World");   //异步执行完成标记
    });
    Thread.Sleep(1100);                     //主线程在执行一些任务
    Console.WriteLine("Main Thread");       //主线程完成标记
    Console.ReadLine();
}
//Hello World
//Main Thread

总结概括

同步和异步主要用于修饰方法。

同步:当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行。

异步:当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕。
在这里插入图片描述

编译器在处理异步方法的时候,会构建一种机制,

该机制可以启动await 语句所要等候的那项异步任务,并使得程序在该工作完成之后,能够用某个线程继续执行await语句后面的那些代码。

这个await语句正是关键所在。编译器会构建相应的数据结构,并把await之后的指令表示成delegate,使得程序在处理完那项异步任务之后,能够继续执行下面的那些指令。

编译器会把当前方法中的每一个局部变量的值都保存在这个数据结构中,并根据await语句所要等候的任务来配置相应的逻辑,让程序能够在该任务完成之后指派某个线程,从await语句的下一条指令开始继续执行。实际上,这相当于编译器生成了一个delegate,用以表示await语句之后的那些代码,并写入了相应的状态信息,用以确保await语句所等候的那项任务执行完毕以后这个delegate能够正确的得到调用。

同步和异步主要用于修饰方法

当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法

当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法

异步的好处在于非阻塞("调用线程"不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率(其实并不会提高运行速度)。

net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await

异步方法旨在成为非阻止操作。 异步方法中的 await 表达式在等待的任务正在运行时不会阻止当前线程。 相反,表达式在继续时注册方法的其余部分并将控制返回到异步方法的调用方

async 和 await 关键字不会创建其他线程。 因为异步方法不会在其自身线程上运行,因此它不需要多线程。 只有当方法处于活动状态时,该方法将在当前同步上下文中运行并使用线程上的时间。 可以使用 Task.Run 将占用大量 CPU 的工作移到后台线程,但是后台线程不会帮助正在等待结果的进程变为可用状态。

对于异步编程而言,该基于异步的方法优于几乎每个用例中的现有方法。 具体而言,此方法比 BackgroundWorker 类更适用于 I/O 绑定操作,因为此代码更简单且无需防止争用条件。 结合 Task.Run 方法使用时,异步编程比 BackgroundWorker 更适用于 CPU 绑定操作,因为异步编程将运行代码的协调细节与 Task.Run 传输至线程池的工作区分开来。

async/await本质上只是一个语法糖Syntactic sugar,它并不产生线程,只是在编译时把语句的执行逻辑改了,相当于过去我们用callback,这里编译器帮你做了

async并不是表明这个方法是异步方法,而是表明这个方法里有异步调用,真正重要的是**await,**他会同步等待异步调用的完成。如果没有使用await关键字,那么该方法就作为一个同步方法。编译器将向我们显示警告,但不会显示任何错误。如果异步方法未使用 await 运算符标记暂停点,则该方法会作为同步方法执行,即使有 async 修饰符,也不例外。

async 和 await 关键字不会创建其他线程。因为异步方法不会在其自身线程上运行,因此它不需要多线程。

如果使用 async 修饰符将某种方法指定为异步方法,即启用以下两种功能。

  1. 标记的异步方法可以使用 await 来指定暂停点await 运算符通知编译器异步方法:在等待的异步过程完成后才能继续通过该点。 同时,控制返回至异步方法的调用方。
  2. 异步方法在 await 表达式执行时暂停并不构成方法退出,只会导致 finally 代码块不运行。标记的异步方法本身可以通过调用它的方法等待。

I/O 绑定和CPU 绑定的不同编程方式:

  1. 如果工作为 I/O 绑定,使用 async 和 await(而不使用 Task.Run)。 不应使用任务并行库。
  2. 如果工作属于 CPU 绑定,并且重视响应能力,使用 async 和 await,但在另一个线程上使用 Task.Run 生成工作。 如果同时适用于并发和并行,应考虑使用任务并行库。

类似于线程池工作项对异步操作的封装,任务是对异步操作的另一种形式的封装

任务启动后,通过任务调度器TaskScheduler来调度。

.NET中提供两种任务调度器

一种是线程池任务调度器,也是默认调度器,它会将任务派发给线程池工作者线程;

另一种是上下文同步任务调度器,它会将任务派发给当前上下文线程

async 方法被编译成一个状态机,结合task调度系统,实现语言运行时的协程

csharp语言内部实现了task的调度器,通过线程池来执行task,当一个task wait的时候,就让出线程,调度别的task在线程上执行

await/async和线程没有具体的关系,只是编译器的语法糖,用于在编译时是否转换为状态机,成为协程(协程也叫纤程),将await变成一个stackless协程由状态机实现

返回值及返回类型:

异步方法通常返回 Task 或 Task。 在异步方法中,await 运算符应用于通过调用另一个异步方法返回的任务

如果方法包含指定 Task 类型操作数的 return 语句,将 TResult 指定为返回类型。

如果方法不含任何 return 语句或包含不返回操作数的 return 语句,将 Task 用作返回类型。

[关于多线程]


Task

Task是在ThreadPool的基础上推出的,我们简单了解下ThreadPool。ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务

创建并运行一个Task,Task的创建和执行方式有如下三种:
static void Main(string[] args)
{
  // 1.new方式实例化一个Task,需要通过Start方法启动
  Task task = new Task(() =>
  {
     Thread.Sleep(100);
     Console.WriteLine($"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId}");
  });
  task.Start();

  // 2.Task.Factory.StartNew(Action action)创建和启动一个Task
  Task task2 = Task.Factory.StartNew(() =>
  {
      Thread.Sleep(100);
      Console.WriteLine($"hello, task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
  });

  // 3*.Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
Task task3 = Task.Run(() =>
{
     Thread.Sleep(100);
     Console.WriteLine($"hello, task3的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
});
     Console.WriteLine("执行主线程!");
     Console.ReadKey();
}

Task 启动的线程默认为线程池里的,启动后默认为后台线程
在这里插入图片描述
无参无返回值

Task.Run(Test);

public void Test()
{
    //...to...

}

无参有返回值

//以string返回值为例,Task<string>中的<string>可省略
//task前面的var也可以直接写Task<string>,这里如果直接写的话不能将<string>省略
var task=Task.Run(Test);
string result=task.Result;

public string Test()
{
    //...todo...

    return "str";
}

有参无返回值

//以string参数为例
string str="str...";
Task.Run(()=>Test(str));

public void Test(string str)
{
    //...todo...

}

有参有返回值

//这里以参数为int,返回值string为例
int num = 10 ;
var task = Task.Run(() => Test(num));
string result = task.Result;

public string Test(int n)
{
    //...todo...

    return "str...";
}

关于同步异步的一些理解误区

应该这样理解它

异步,早期开发人员对它有很多误解,认为不阻塞主线程就是异步,更有认为**不阻塞UI就是异步,但异步归根结底和这两个东西关系并不大,异步的出现主要是为了提高线程的利用率,让可用线程率更高,而不是一个线程只做一件事,这件事没有完成就不去做下面的事情,这是不正确的,线程应该被解放出来!事实上,你如果学过nodejs的话,对单线程非阻塞应该更清楚一些,它主要通过方法回调来实现异步的**,只是在语法上和C#不太一样。

说一下上面提到的误解

误解1:不阻塞主线程

如果不阻塞主线程的话,你只能开个新线程完成这个动作,像一些系统通知,它和主线程的工作流程没有关系,**如果开个新线程,与主线程并行执行,这并不是我们说的异步,这只是多线程!**它会增加线程的开支,使用不当,会影响系统的吞吐量!

误解2:不阻塞UI

这就更属于胡扯了,对于一个工作流来说,必须要按着1,2,3的顺序去执行,如果是同步代码,它是一个线程从1执行到3,这个线程将一直被占用!如果是异步代码,它在执行到1时,线程被回收到池子,其它人可以使用,当1执行完成后,从线程池里取出一个新的线程继续执行这叫异步!C#的异步进行友好,使用async,await就可以实现了!

异步会提高程序的运行速度吗

多线程会提高程序的效率,不会提高运行速度。

这就好比这一个任务让前台花1个小时。前台完成10分钟的时候

打电话给经理,让他安排一个人来干30分钟(new Thread()),他干剩下的20分钟。(创建线程,需要时间,内存资源)

或者从旁边空闲的同事中(ThreadPool 或 Task),拉一个人过来干30分钟。他干剩下的20分钟。(需要的时间少,资源本来就存在)

从上看出,异步会让一份任务时间变长。资源消耗更多。但是可以让前台(UI线程)空闲下来,听从领导(用户)指挥。


同步方法与普通方法的区别

“等待结果”的本质

无论是同步方法还是普通写法(这里指“普通同步写法”),它们的核心共同点都是:程序必须等待某个操作完成后才能继续执行下一步。 这种“等待”意味着当前线程会被阻塞,直到操作返回结果。

同步方法与普通同步写法的区别

主要区别在于代码的组织方式和语义表达,以及在某些情况下,同步方法可能提供的额外功能

同步方法

通常是指那些设计为同步执行的函数或方法。它们明确地表示了“等待结果”的行为。

例如,File.ReadAllText() 就是一个同步方法,它清楚地表明了“读取文件内容并等待结果”的意图。

同步方法往往具有更清晰的语义,便于开发者理解代码的执行流程。

这是一种将“普通同步写法”封装起来的方式。

它关注的是“完成一个特定任务”。

同步方法通常完成一个有明确目的的操作,例如“读取整个文件”、“从数据库获取数据”等。

它隐藏了实现的细节,让调用者只需要关注结果。

它更关注的是“结果”。

普通同步写法

这是一种最基础的代码编写方式。

代码按照顺序一行接一行地执行,每行代码执行完毕后,才会执行下一行。

它关注的是“如何一步一步地完成任务”。

例如,你写一个循环来计算总和,或者读取一个简单的变量,这些都属于普通同步写法。

它更关注的是“过程”。

指的是那些没有被特别标记为“同步方法”,但实际上也是同步执行的代码。

例如,一个简单的循环计算或者对一个内存中数据的处理,通常都是普通同步写法。

普通同步写法可能没有明确的“等待结果”的语义,但它仍然是同步执行的。

额外功能

同步方法某些同步方法可能提供额外的功能,例如:

错误处理:同步方法通常会直接抛出异常,便于开发者进行错误处理。

资源管理:某些同步方法可能会自动管理资源,例如文件句柄的打开和关闭。

这些额外功能可以简化代码的编写,提高代码的健壮性。

普通同步写法:

普通同步写法通常需要开发者手动处理错误和资源管理。

// 同步方法:
int result = SomeSynchronousMethod();
SomeSynchronousMethod() 是一个同步方法,它会阻塞当前线程,直到返回结果。

// 普通同步写法:
int result = 0; for (int i = 0; i < 1000; i++) { result += i; }
这段代码也是同步执行的,但它没有被特别标记为“同步方法”。

普通同步写法:
你可以一步一步地写下做饭的步骤:
“打开冰箱。”
“取出鸡蛋。”
“打开燃气灶。”
“打鸡蛋到锅里。”
“等待鸡蛋煎熟。”
这就像你直接写代码的每一行。

同步方法:
你可以创建一个“煎鸡蛋”的同步方法: 煎鸡蛋() 这个“煎鸡蛋”方法内部包含了打开冰箱、取出鸡蛋、煎熟鸡蛋等一系列步骤。 你调用“煎鸡蛋”方法时,只需要等待鸡蛋煎好,不需要关心内部的细节。 这就好比你调用一个函数。

总结:

  • 普通同步写法是代码的“基本单元”,它关注的是“过程”。
  • 同步方法是代码的“封装单元”,它关注的是“结果”。
  • 同步方法,是将许多步骤和过程封装到一个方法中,对外只提供一个简单的调用方式,返回一个结果。
  • 普通同步写法,是将所有的步骤,都直接写在调用方。
  • 同步方法的好处是,可以提高代码的复用性,简化代码的复杂性。

await 关键字用于暂停当前方法的执行,直到异步操作完成。 那不是await 会阻塞后面的代码执行么

你提出了一个非常关键的问题,这也是很多人对 await 产生困惑的地方。await 确实会暂停当前方法的执行,但它不会阻塞整个线程,这就是它与传统同步阻塞操作的根本区别。

为了更好地理解,让我们深入探讨一下 await 的工作原理:

1. await 暂停的是当前 async 方法的执行

  • 当你使用 await 关键字时,它只会暂停当前被 async 关键字标记的异步方法的执行。
  • 它不会阻塞调用该异步方法的线程,也不会阻塞其他线程的执行。

2. await 如何实现非阻塞

await 遇到一个未完成的异步操作时,它会:

  • 将当前异步方法的控制权返回给调用者
  • 注册一个回调函数,以便在异步操作完成后恢复当前方法的执行。
  • 这意味着,在异步操作进行期间,调用者可以继续执行其他任务,而不会被阻塞
  • 异步操作完成时回调函数会被触发await恢复当前异步方法的执行。

3. await 与线程的关系

  • await 并不创建或管理线程。
  • 它主要用于管理异步操作的执行流程,而无需显式地操作线程。
  • 在大多数情况下,await 会在同一个线程上恢复异步方法的执行,但这不是绝对的。

4. 举例说明

  • 假设你有一个异步方法 DownloadFileAsync(),用于从网络下载文件。

当你使用 await DownloadFileAsync() 时,它会:

  • 发起网络请求,开始下载文件。
  • 暂停 DownloadFileAsync() 方法的执行。
  • 将控制权返回给调用者,允许调用者执行其他任务。
  • 当文件下载完成后,恢复 DownloadFileAsync() 方法的执行。
  • 在这个过程中,调用者可以继续响应用户界面、处理其他请求等,而不会被文件下载操作阻塞。

总结

  • await 会暂停当前 async 方法的执行,但不会阻塞整个线程。
  • 它通过回调函数和状态机等机制,实现了非阻塞的异步操作。
  • 这使得程序可以在等待异步操作完成时,继续执行其他任务,从而提高程序的响应速度和并发性。
  • 简单来说,await 暂停的是当前async标记的方法,不是线程。

那await后面的代码就会在async 异步方法结束后,才执行么

是的,你的理解是正确的。await 关键字的一个核心作用就是确保 async 异步方法中 await 后面的代码,在被 await 的异步操作完成后才执行。

为了更清晰地解释,我们来分解一下 await 的执行流程:

1. 遇到 await:暂停执行

async 方法执行到 await 关键字时,它会:

  • 检查被 await 的异步操作(通常是一个 Task)是否已经完成。
  • 如果异步操作已经完成,则继续执行 await 后面的代码。
  • 如果异步操作尚未完成,则暂停当前 async 方法的执行。

2. 异步操作完成:恢复执行

当被 await 的异步操作完成时,运行时环境会:

  • 恢复之前被暂停的 async 方法的执行。
  • await 表达式中获取异步操作的结果(如果异步操作有结果)。
  • 继续执行 await 后面的代码。

3. 代码执行顺序

因此,await 关键字确保了 async 方法中的代码按照以下顺序执行:

  • await 之前的代码。
  • await 的异步操作。
  • await 后面的代码。
async Task MyAsyncMethod()
{
    Console.WriteLine("1. 开始异步操作");
    await Task.Delay(1000); // 模拟一个耗时 1 秒的异步操作
    Console.WriteLine("2. 异步操作完成");
    Console.WriteLine("3. 继续执行");
}

在这个例子中,输出的顺序将是:

  • “1. 开始异步操作”
  • (等待 1 秒)
  • “2. 异步操作完成”
  • “3. 继续执行”
  • await Task.Delay(1000) 确保了 “2. 异步操作完成” 和 “3. 继续执行” 这两行代码,在 Task.Delay(1000) 模拟的延迟操作完成后才执行。

总结:

  • await 关键字是 async 异步方法中控制代码执行顺序的关键。
  • 它确保了 await 后面的代码在被 await 的异步操作完成后才执行,从而实现了有序的异步编程。
  • 这样既保证了异步操作,又保证了代码执行的顺序性

Task.Run我是理解成多线程,还是异步?

  • Task.Run 本质上是多线程的体现,但它服务于异步编程。
  • Task.Run 的核心作用是将一个任务(一段代码)放到线程池中执行。线程池是一组预先创建的线程,用于执行各种任务,从而避免了频繁创建和销毁线程的开销。
  • 因此,Task.Run 确实涉及到了多线程。它使得任务可以在另一个线程上并行执行,从而不会阻塞当前线程。
  • 但是,Task.Run 的目的不仅仅是多线程。它是为了支持异步编程模型。通过将耗时操作放到后台线程执行Task.Run 使得程序可以继续执行其他任务,提高了程序的响应性和并发性。
  • 简单的说,Task.run 本身就是多线程,通过多线程,实现了异步的效果。

立即输出后面的话,然后等待耗时操作结束后,才输出耗时操作的结果

//示例 1:使用 Task.Run 和 async/await
using System;
using System.Threading.Tasks;

public class AsyncExample
{
    public static async Task Main(string[] args)
    {
        Console.WriteLine("1. 立即输出:开始异步操作");

        // 使用 Task.Run 将耗时操作放入线程池
        Task<int> longRunningTask = Task.Run(() =>
        {
            // 模拟耗时操作
            Task.Delay(2000).Wait(); // 模拟耗时2秒
            return 42;
        });

        Console.WriteLine("2. 立即输出:异步操作进行中...");

        // 等待耗时操作完成,并输出结果
        int result = await longRunningTask;
        Console.WriteLine($"3. 耗时操作结果:{result}");

        Console.WriteLine("4. 异步操作完成");
    }
}
/*
首先,立即输出 "1. 立即输出:开始异步操作"。
然后,使用 Task.Run 将耗时操作(模拟延迟 2 秒)放入线程池中执行。
立即输出 "2. 立即输出:异步操作进行中...",表示异步操作已经开始。
使用 await longRunningTask 等待耗时操作完成,并获取结果。
耗时操作完成后,输出 "3. 耗时操作结果:{result}"。
最后,输出 "4. 异步操作完成"。
*/

//示例 2:使用 Task.ContinueWith
using System;
using System.Threading.Tasks;

public class AsyncExampleContinueWith
{
    public static void Main(string[] args)
    {
        Console.WriteLine("1. 立即输出:开始异步操作");

        // 使用 Task.Run 将耗时操作放入线程池
        Task<int> longRunningTask = Task.Run(() =>
        {
            // 模拟耗时操作
            Task.Delay(2000).Wait(); // 模拟耗时2秒
            return 42;
        }); //用于将耗时操作放入线程池,避免阻塞主线程

        Console.WriteLine("2. 立即输出:异步操作进行中...");

        // 使用 ContinueWith 在耗时操作完成后输出结果
        longRunningTask.ContinueWith(task =>
        {
            Console.WriteLine($"3. 耗时操作结果:{task.Result}");
            Console.WriteLine("4. 异步操作完成");
        });

        // 这里可以执行其他操作,不会被耗时操作阻塞
        Console.WriteLine("5. 主线程继续执行...");

        // 等待任务完成,确保主线程不会提前结束
        longRunningTask.Wait();
    }
}
//await 或 ContinueWith 用于在耗时操作完成后执行后续操作,并获取结果。
//在耗时操作进行期间,主线程可以继续执行其他任务,从而实现异步执行。
/*
Console.WriteLine("5. 主线程继续执行..."); 会立即输出,因为 Task.Run 是异步的。
在示例 2 中,Task.Run 创建的任务在后台线程中执行。这意味着,当 Task.Run 被调用时,它会立即将任务放入线程池,然后立即返回,而不会等待任务完成。
因此,Console.WriteLine("2. 立即输出:异步操作进行中...")
之后的 Console.WriteLine("5. 主线程继续执行...") 会立即执行,
而无需等待 Task.Run 中的耗时操作完成。
Task.ContinueWith 方法注册的回调函数,
只会在Task.Run中的耗时操作完成之后才会被调用。
longRunningTask.Wait();这行代码的目的是,
让主线程等待longRunningTask执行完成,不然主线程执行完成,程序就结束了,
而longRunningTask还在执行中,就会被强制结束。
*/

遇到的关于是否等待异步方法及返回值的问题

public async static void SimpleTaskRunAwait()
         {
             Console.WriteLine("1. 立即输出:开始异步操作");

             // 使用 Task.Run 将耗时操作放入线程池
             Task<int> longRunningTask = Task.Run(() =>
             {
                 // 模拟耗时操作
                 Task.Delay(5000).Wait(); // 模拟耗时2秒
                 return 42;
             });

             Console.WriteLine("2. 立即输出:异步操作进行中...");

             // 等待耗时操作完成,并输出结果
             int result = await longRunningTask;
             Console.WriteLine($"3. 耗时操作结果:{result}");

             Console.WriteLine("4. 异步操作完成");
             Console.ReadKey();
         }
// 为什么我控制台运行这段代码,直接闪了一下就关了

你遇到的问题是由于 SimpleTaskRunAwait 方法没有被正确调用和等待导致的。SimpleTaskRunAwait 是一个 async static void 方法,这意味着它是一个异步方法,但它没有返回任何 Task 对象。因此,当你在控制台应用程序中直接调用它时,程序会立即执行到 SimpleTaskRunAwait 方法的末尾,然后退出,而不会等待异步操作完成。

需要确保 SimpleTaskRunAwait 方法被正确调用和等待

1. 在 Main 方法中等待异步方法完成:

如果你的 Main 方法可以修改,这是最推荐的方法。你需要将 Main 方法也声明为 async static Task,然后使用 await 关键字等待 SimpleTaskRunAwait 方法完成。

public class Program
{
    public static async Task Main(string[] args)
    {
        await SimpleTaskRunAwait();
    }

    public async static Task SimpleTaskRunAwait() // 异步方法
    {
        Console.WriteLine("1. 立即输出:开始异步操作");

        // 使用 Task.Run 将耗时操作放入线程池
        Task<int> longRunningTask = Task.Run(() =>
        {
            // 模拟耗时操作
            Task.Delay(5000).Wait(); // 模拟耗时5秒
            return 42;
        });

        Console.WriteLine("2. 立即输出:异步操作进行中...");

        // 等待耗时操作完成,并输出结果
        int result = await longRunningTask;
        Console.WriteLine($"3. 耗时操作结果:{result}");

        Console.WriteLine("4. 异步操作完成");
        Console.ReadKey();
    }
}

2. 使用 Task.Run Wait() 方法等待异步方法完成:

如果你的 Main 方法不能修改,你可以使用 Task.RunWait() 方法来等待异步方法完成。

public class Program
{
    public static void Main(string[] args)
    {
        Task.Run(async () => await SimpleTaskRunAwait()).Wait();
    }

    public async static void SimpleTaskRunAwait()
    {
        Console.WriteLine("1. 立即输出:开始异步操作");

        // 使用 Task.Run 将耗时操作放入线程池
        Task<int> longRunningTask = Task.Run(() =>
        {
            // 模拟耗时操作
            Task.Delay(5000).Wait(); // 模拟耗时5秒
            return 42;
        });

        Console.WriteLine("2. 立即输出:异步操作进行中...");

        // 等待耗时操作完成,并输出结果
        int result = await longRunningTask;
        Console.WriteLine($"3. 耗时操作结果:{result}");

        Console.WriteLine("4. 异步操作完成");
        Console.ReadKey();
    }
}

3. 将 SimpleTaskRunAwait 方法改为返回 Task:

修改SimpleTaskRunAwait 方法,返回Task,然后通过Task.Wait()方法等待完成。

public class Program
{
    public static void Main(string[] args)
    {
        SimpleTaskRunAwait().Wait();
    }

    public async static Task SimpleTaskRunAwait()
    {
        Console.WriteLine("1. 立即输出:开始异步操作");

        // 使用 Task.Run 将耗时操作放入线程池
        Task<int> longRunningTask = Task.Run(() =>
        {
            // 模拟耗时操作
            Task.Delay(5000).Wait(); // 模拟耗时5秒
            return 42;
        });

        Console.WriteLine("2. 立即输出:异步操作进行中...");

        // 等待耗时操作完成,并输出结果
        int result = await longRunningTask;
        Console.WriteLine($"3. 耗时操作结果:{result}");

        Console.WriteLine("4. 异步操作完成");
        Console.ReadKey();
    }
}

await一定要配合async一起使用么

是的,await 关键字必须与 async 关键字一起使用。它们是 C# 中实现异步编程的两个关键组成部分,缺一不可。

以下是它们之间关系的详细解释:

1. async 关键字的作用:

  • async 关键字用于标记一个方法为异步方法
  • 它告诉编译器,该方法可能包含异步操作,并且可以使用 await 关键字。
  • 编译器会生成一个状态机,用于管理方法的异步执行流程。

2. await 关键字的作用:

  • await 关键字用于暂停当前异步方法的执行,直到被等待的异步操作完成。
  • 只能在被 async 关键字标记的方法中使用
  • await 关键字只能等待 Task Task<T> 对象

3. 它们之间的依赖关系:

  • await 关键字必须 async 方法中使用,否则编译器会报错。
  • async 关键字本身并不执行任何异步操作,它只是允许方法中使用 await 关键字
  • asyncawait 关键字共同控制异步方法的执行流程,实现非阻塞的异步编程。

4. 示例说明:

using System;
using System.Threading.Tasks;

public class AsyncAwaitExample
{
    public static async Task Main(string[] args)
    {
        Console.WriteLine("开始异步操作...");

        // 使用 await 关键字等待异步操作完成
        await Task.Delay(1000);

        Console.WriteLine("异步操作完成。");
    }
}
  • 在这个例子中,Main 方法被标记为 async Task,因为它使用了 await Task.Delay(1000)
  • await 关键字暂停了 Main 方法的执行,直到 Task.Delay(1000) 完成。

5. 总结:

  • await 关键字必须与 async 关键字一起使用。
  • async 关键字允许方法中使用 await 关键字,实现异步编程。
  • 它们共同控制异步方法的执行流程,实现非阻塞的异步操作。

异步编程陷阱

陷阱1:阻塞异步代码(async+.Result/Wait)

在这里插入图片描述

陷阱2:async void

在这里插入图片描述

陷阱3:忽略ConfigAwait(false)

在这里插入图片描述

陷阱4:过度并行烧毁CPU

在这里插入图片描述

陷阱5:未释放CancellationTokenSource

在这里插入图片描述

陷阱6:错误处理异步异常

错误写法:

catch不到异常

try
{
  Task.Run(()=>
  {
    throw new Exception();
  })
}
catch
{
  /*正常捕获*/
}

正确写法:

await任务捕获异常

try
{
  await Task.Run(...);
}
catch
{
  /*正常捕获*/
}
陷阱7:异步Lambda中的闭包陷阱

错误写法:

for (int i = 0; i < 10; i++)
{
    Task.Run(async() => await Process(i)); // 所有i最终都是10!
}

正确写法:

for (int i = 0; i < 10; i++)
{
    var temp = i;
    Task.Run(async() => await Process(i)); // 局部变量拷贝
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐