C#----异步编程

发布于:2025-09-04 ⋅ 阅读:(16) ⋅ 点赞:(0)

1 多线程和异步的区别

多线程 ≠ 异步

  1. 多线程经常会阻塞,而异步不阻塞
  2. 多线程和异步的使用场景不同

1.1 多线程使用场景

  • 适合CPU密集型操作

  • 适合长期运行的任务
    多线程适合比较耗时的任务,或者需要长期运行的任务,它会充分调动CPU的某一个核去进行相应的计算。使用多线程很多的一个场景就是,在后台开一个轮询。

  • 线程的创建销毁开销比较大
    异步默认借助线程池,线程池的开销就小很多。

  • 提供更底层的控制,操作线程,锁,信号
    线程是一个比较底层的功能,所以提供了更底层的控制,比如线程锁,信号量等等,但是异步其实对于线程池的封装,这样就不能很好地进行底层的操作。

  • 线程不易于传参及返回
    传参的话可以在Thread.Start里面去传参,但是如果你希望它返回一个结果,那就有点麻烦了,你可能不得不借助全局变量,或者回调函数,同时你还需要在某些地方去阻塞,从而等待某个线程的结束,这样才能保证那个线程已经计算出了结果。

  • 线程的代码书写较为繁琐
    尤其是当你任务比较多,任务之间可能还有一些顺序先后关系,这种情况下你的代码可能
    会显得非常繁琐,但是如果采用异步编程,使用async await 可以非常灵活的去实现这些功能。

1.2 异步使用场景

  • 适合IO密集型操作

  • 适合短暂的小任务
    若频繁访问IO操作,这种情况下比较适用异步编程,因为它比较适合短暂且频繁的小任务,而不是使用多线程。因为线程开出来后,你肯定是希望它多干一会活,毕竟线程的创建开销比较大。但是如果只是一个几十,几百毫秒的小任务,这时候使用多线程就显得不划算了。

  • 避免线程阻塞,提高系统的响应能力
    更重要的是,它不是线程,它不会阻塞线程,所以它可以提高系统的响应能力。一种使用场景是,在UI线程上调用一个方法,若这个方法阻塞了,则UI就会卡顿,这种情况下就需要使用异步。

2 异步任务

2.1 Task 是一个包含异步任务状态的引用类型


            task.Id.ToString(); // 获取任务的唯一标识符

            task.IsFaulted.ToString(); // 获取一个值,该值指示任务是否已因未处理的异常而完成

            task.IsCanceled.ToString(); // 获取一个值,该值指示任务是否已被取消

            task.IsCompleted.ToString(); // 获取一个值,该值指示任务是否已完成
          
            task.Exception?.ToString(); // 获取导致任务失败的未处理异常的聚合

            task.CreationOptions.ToString(); // 获取任务的创建选项

            task.Status.ToString(); // 获取任务的当前状态

2.1 Task 是对于异步任务的抽象

  1. 开启了异步任务之后,当前线程并不会阻塞,而是去做其它事情
  2. 异步任务(默认)会借助线程池在其它线程上运行
  3. 获取结果后回到之前的状态

2.3 任务的结果

1.返回值为Task 表示异步任务没有返回值

  1. 返回值为Task 则表示异步任务有类型为T的返回值

3 异步方法 (Async Task)

3.1 将异步方法标记为async 之后,就可以在方法中使用await 关键字

await XXX 这个XXX 可以是 Task 也可以是 async Task 类型。

3.2 await 关键字会等待异步任务的结束,并获得结果

await 前后会切换线程

若不想切换线程可使用 Task 的 ConfigureAwait(true); //这里配置ConfigureAwait为true,可以回到之前的线程

你想回到之前的线程,你必须得有一个同步上下文。但是对于一个控制台应用程序,它默认是没有上下文的。只有Windform WPF这些程序,也就是UI线程,它默认是有同步上下文的。因为它要保证UI线程需要拥有UI线程上的全部的资源,比如按钮,TextBox这些东西,任何的这些操作都需要发生在UI线程上,而控制台程序就没有这些要求了。
所以,这里总的来说await关键字会等待异步任务的结束,并获得结果,这里的await Task.Delay(1000)就相当于一个异步任务,但是这里是没有返回值的。再比如我们看下面这个例子:

3.3 async + await 会将方法包装成状态机,await 类似于检查点

3.4 Async Task

  1. 返回值依旧是Task 类型 但是在其中可以使用 await 关键字
  2. 在其中写返回值可以直接写Task 中返回类型
public Task Run1()
{
    return Task.Delay(1000);  // 必须返回Task
}


public async Task Run2()
{
    await Task.Delay(1000);    // 没有返回值 
}
public async Task<string> Run3()
{
    await Task.Delay(1000);    // 没有返回值 
    return "Hello";  // 返回值
}

3.4 Async void

  1. 同样是状态机,但缺失记录状态的Task 对象
    Task 状态机是 AsyncTaskMethodBuilder 现在是 AsyncVoidMethodBuilder
    async void 方法里面可以使用await, 但是调用的地方没办法使用await ,因为它的返回值是void,不是Task 。所以也没办法知道异步任务的返回值和状态。
public async void Run4()
{
    await Task.Delay(1000);    // 没有返回值 
}

public async void Run5()
{
    Run4();    // 这里没办法用 await
}
  1. 无法聚合异常(AggregateException), 需谨慎处理异常
    最需要注意的是,Async void 没有办法获取异常任务的报错信息。如果有Task,Task 会把收集异常报错信息做成汇总,就叫做AggregateException 聚合异常,现在没有Task ,就没有办法实现这个功能。报错会直接抛出。

async void 捕获不了异常
在这里插入图片描述
async Task 可捕获到异常

修改成 Run7().wait()
在这里插入图片描述
修改成 Run7().GetAwaiter().GetResult();
在这里插入图片描述
3. 几乎只对于事件的注册
上面提到 async void 这么危险 但 并不是所有的情况下都使用 async task 就可以了呢 ,有一种情况根本用不了 async Task,那就是对于事件的注册,尤其是WPF里面的Click 这些事件。

3.6 异步编程具有传染性

  1. 一处async 处处async
  2. 几乎所有自带的方法都提供了异步的版本
    。。。

4 异步编程的重要思想

  1. await 会暂时释放当前线程,使得线程可以执行其它工作,而不必阻塞线程直到异步操作完成。

  2. 不要在异步方法里面用任何方式阻塞当前线程

  3. 常见的阻塞情形

  • Task.wait() Task.Result
    若任务没有完成,就会阻塞当前线程,容易造成死锁

  • Task.Delay() Thread.sleep()

  • IO 同步操作的方法

  • 其它繁重且耗时的任务

5 同步上下文

  1. 一中管理和协调线程的机制,允许开发者将代码的执行切换到特点的线程。
    前面异步任务里面线程的切换,都是借助同步上下文来实现的

  2. Winform 和 WPF 具有同步的上下文(UI 线程) ,控制台程序没有

  3. ConfigureAwait(false)

配置人通过await方法结束后是否会回到原来的线程,默认为True,一般只有UI线程会采用这种方式。可以看一下下面代码:

private void Button_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");

    TestAsync().ConfigureAwait(true);

    Debug.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
}


public async Task TestAsync()
{
    await Task.Delay(1000);
    return;
}

打印结果
在这里插入图片描述

若 不回到之前的线程 则前后线程不一致 ,若更改了控件则会报错,因为控件必须在UI线程上进行修改。

private async void Button_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");

    await TestAsync().ConfigureAwait(false);

    this.btn.Content = "完成";

    Debug.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
}


public async Task TestAsync()
{
    await Task.Delay(1000);
    return;
}

在这里插入图片描述
还要注意一个地方:

int result = TestAsync().Result; // 容易造成死锁

 private async void Button_Click(object sender, RoutedEventArgs e)
 {
     Debug.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");

     int result = TestAsync().Result;

     this.btn.Content = "完成";

     Debug.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
 }


 public async Task<int> TestAsync()
 {
     await Task.Delay(1000);
     return 20;
 }

这里调用.Result阻塞了UI线程,然后TestAsync()方法里面,await Task.Delay(2000);之后希望回到UI线程,可是现在UI线程被阻塞,它需要等异步任务执行完才会释放,所以导致了死锁。解决方法为:
async Task TestAsync()
{
await Task.Delay(2000).ConfigureAwait(false);
return 10;
}

异步任务 需要 UI线程 但UI线程现在处于阻塞状态
UI线程需要 异步任务执行完成 才会释放

5 TaskScheduler

TaskScheduler也可以用来配置同步上下文,控制Task的调度和运行线程,还有设置优先级,上下文及执行状态等。

6. 异步任务的创建

  1. Task.Run()

  2. Task.Factory.StartNew()

Task.Factory.StartNew()相当于Task.Run()的完整版,提供了比Task.Run()更多的功能,比如TaskCreationOptions.LongRunning标记它是一个长时间运行的线程

  1. new Task + Task.Start()
    跟new Thread 和 Thread.Start很像