C# 异步编程详解(Task,async/await)

明明明h 2024-10-06 13:05:05 阅读 100

文章目录

1.什么是异步2.Task 产生背景3.Thread(线程) 和 Task(异步)的区别3.1 几个名词3.2 Thread 与 Task 的区别

4.Task API4.1 创建和启动任务4.2 Task 等待、延续和组合4.3 task.Result4.4 <code>Task.Delay() 和 Thread.Sleep() 区别

5.CancellationToken 和 CancellationTokenSource 取消线程5.1 CancellationToken5.2 CancellationTokenSource5.3 示例

6.asyncawait7.微软案例

1.什么是异步

同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。

异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。

2.Task 产生背景

Task出现之前,微软的多线程处理方式有:Thread→ThreadPool→委托的异步调用,虽然也可以基本业务需要的多线程场景,但它们在多个线程的等待处理方面、资源占用方面、线程延续和阻塞方面、线程的取消方面等都显得比较笨拙,在面对复杂的业务场景下,显得有点捉襟见肘了。

ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如:

ThreadPool不支持线程的取消、完成、失败通知等交互性操作;ThreadPool不支持线程执行的先后次序;

正是在这种背景下,Task应运而生。Task是微软在.Net 4.0时代推出来的,也是微软极力推荐的一种多线程的处理方式,Task看起来像一个Thread,实际上,它是在ThreadPool的基础上进行的封装,Task的控制和扩展性很强,在线程的延续、阻塞、取消、超时等方面远胜于Thread和ThreadPool。以下是一个简单的任务示例:

static void Main(string[] args)

{ -- -->

Task t = new Task(() =>

{

Console.WriteLine("任务开始工作……");

Thread.Sleep(5000); //模拟工作过程

});

t.Start();

t.ContinueWith(task =>

{

Console.WriteLine("任务完成,完成时候的状态为:");

Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}",

task.IsCanceled, task.IsCompleted, task.IsFaulted);

});

Console.ReadKey();

}

3.Thread(线程) 和 Task(异步)的区别

3.1 几个名词

1、进程(process): 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的。2、线程(thread): 线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

前台线程: 前台线程是不会被立即关闭的,它的关闭只会发生在自己执行完成时,不受外在因素的影响。假如应用程序退出,造成它的前台线程终止,此时CLR仍然保持活动并运行,使应用程序能继续运行,当它的的前台线程都终止后,整个进程才会被销毁。(Thread类默认创建的是前台线程)后台线程: 后台线程是可以随时被CLR关闭而不引发异常的,也就是说当后台线程被关闭时,资源的回收是立即的,不等待的,也不考虑后台线程是否执行完成,就算是正在执行中也立即被终止。(通过线程池/Task创建的线程都是后台线程) 3、同步(sync): 发出一个功能调用时,在没有得到结果之前,该调用就不返回。4、异步(async): 与同步相对,调用在发出之后,这个调用就直接返回了,所以没有返回结果。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。

通知调用者的三种方式:

状态:即监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。通知:当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。回调:与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。 5、阻塞(block): 阻塞调用是指调用结果返回(或者收到通知)之前,当前线程会被挂起,即不继续执行后续操作。简单来说,等前一件做完了才能做下一件事。6、非阻塞(non-block): 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

3.2 Thread 与 Task 的区别

Thread 类主要用于实现线程的创建以及执行。

Task 类表示以异步方式执行的单个操作。

1、Task 是基于 Thread 的,是比较高层级的封装,Task 最终还是需要 Thread 来执行

2、Task 默认使用后台线程执行,Thread 默认使用前台线程

static void Main(string[] args)

{

Thread thread = new Thread(obj => { Thread.Sleep(3000); });

thread.Start();

}

// 上面代码,tread为前台线程,主程序在3秒后结束。

static void Main(string[] args)

{

Task<int> task = new Task<int>(() =>

{

Thread,Sleep(3000);

return 1;

});

task.Start();

}

// 上面代码,task为后台线程,主程序会瞬间结束。

3、Task 可以有返回值,Thread 没有返回值

public static void Main(string[] args)

{

Task<int> task = new Task<int>(LongRunningTask);

task.Start();

Console.WriteLine(task.Result);

}

private static int LongRunningTask()

{

Thread.Sleep(3000);

return 1;

}

4、Task 可以执行后续操作,Thread 不能执行后续操作

4.Task API

4.1 创建和启动任务

不带返回值:

//1. new方式实例化一个Task,需要通过Start方法启动

Task task1 = new Task(() =>

{

Thread.Sleep(100);

Console.WriteLine($"hello, task1的线程ID为{ Thread.CurrentThread.ManagedThreadId}");

});

task1.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();

执行主线程!

hello, task1的线程ID为4

hello, task2的线程ID为6

hello, task3的线程ID为7

带返回值:

// 1.new方式实例化一个Task,需要通过Start方法启动

Task<string> task1 = new Task<string>(() =>

{

return $"hello, task1的ID为{ Thread.CurrentThread.ManagedThreadId}";

});

task1.Start();

// 2.Task.Factory.StartNew(Func func)创建和启动一个Task

Task<string> task2 =Task.Factory.StartNew<string>(() =>

{

return $"hello, task2的ID为{ Thread.CurrentThread.ManagedThreadId}";

});

// 3.Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task

Task<string> task3= Task.Run<string>(() =>

{

return $"hello, task3的ID为{ Thread.CurrentThread.ManagedThreadId}";

});

Console.WriteLine("执行主线程!");

Console.WriteLine(task1.Result);// 注意task.Result获取结果时会阻塞UI主线程

Console.WriteLine(task2.Result);

Console.WriteLine(task3.Result);

Console.ReadKey();

执行主线程!

hello, task1的ID为4

hello, task2的ID为6

hello, task3的ID为7

4.2 Task 等待、延续和组合

Wait: 针对单个Task的实例,可以task1.wait进行线程等待(阻塞主线程)WaitAny: 线程列表中任何一个线程执行完毕即可执行(阻塞主线程)WaitAll: 线程列表中所有线程执行完毕方可执行(阻塞主线程)WhenAny: 与ContinueWith配合,线程列表中任何一个执行完毕,则继续ContinueWith中的任务(开启新线程,不阻塞主线程)WhenAll: 与ContinueWith配合,线程列表中所有线程执行完毕,则继续ContinueWith中的任务(开启新线程,不阻塞主线程)ContinueWith: 与WhenAny或WhenAll配合使用ContinueWhenAny: 等价于Task的WhenAny+ContinueWithContinueWhenAll: 等价于Task的WhenAll+ContinueWith

//创建一个任务

Task<int> task = Task.Run<int>(() =>

{

int sum = 0;

Console.WriteLine("使用`Task`执行异步操作.");

for (int i = 0; i < 1000; i++)

{

sum += i;

}

return sum;

});

Console.WriteLine("主线程执行其他处理");

//任务完成时执行处理。

Task cwt = task.ContinueWith(t =>

{

Console.WriteLine("任务完成后的执行结果:{0}", t.Result.ToString());

});

task.Wait();

cwt.Wait();

Action<string,int> log = (name,time) =>

{

Console.WriteLine($"{ name}任务开始...");

Thread.Sleep(time);

Console.WriteLine($"{ name}任务结束!");

};

List<Task> tasks = new List<Task>

{

Task.Run(() => log("张三",3000)),

Task.Run(() => log("李四",1000)),

Task.Run(() => log("王五",2000))

};

//以下语句逐个测试效果

Task.WaitAny(tasks.ToArray());

Task.WaitAll(tasks.ToArray());

Task.WhenAny(tasks.ToArray()).ContinueWith(x => Console.WriteLine("某个Task执行完毕"));

Task.WhenAll(tasks.ToArray()).ContinueWith(x => Console.WriteLine("所有Task执行完毕"));

Task.Factory.ContinueWhenAny(tasks.ToArray(), x => Console.WriteLine("某个Task执行完毕"));

Task.Factory.ContinueWhenAll(tasks.ToArray(), x => Console.WriteLine("所有Task执行完毕"));

Console.ReadKey();

4.3 task.Result

等待获取task返回值,阻塞调用其他线程,直到当前异步操作完成,相当于调用wait方法

static void Main(string[] args)

{

Task<string> task = Task.Run<string>(() =>

{

Thread.Sleep(3000);

return "ming_堵塞线程";

});

Console.WriteLine(task.Result);

Console.WriteLine("主线程执行");

Console.ReadKey();

}

ming_堵塞线程

主线程执行

4.4 Task.Delay()Thread.Sleep() 区别

Thread.Sleep()是同步延迟, Task.Delay()是异步延迟。Thread.Sleep()会阻塞线程, Task.Delay()不会。Thread.Sleep()不能取消, Task.Delay()可以。Task.Delay()Thread.Sleep()最大的区别是Task.Delay()旨在异步运行,在同步代码中使用Task.Delay()是没有意义的;在异步代码中使用Thread.Sleep()是一个非常糟糕的主意。通常使用await关键字调用Task.Delay()

// 阻塞,出现CPU等待...

static void Main(string[] args)

{

// 阻塞,出现CPU等待...

Task.Factory.StartNew(() =>

{

Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ****** Start Sleep()******");

for (int i = 1; i <=10; i++)

{

Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + "******Sleep******==>" + i);

Thread.Sleep(1000);//同步延迟,阻塞一秒

}

Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ******End Sleep()******");

Console.WriteLine();

});

// 不阻塞

Task.Factory.StartNew(() =>

{

Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ======StartDelay()======");

for (int i =1; i <=10; i++)

{

Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ======Delay====== ==>" + i);

Task.Delay(1000);//异步延迟

}

Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ======End Delay()======");

Console.WriteLine();

});

// 不阻塞等待三秒

Task.Factory.StartNew(async() =>

{

Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ======StartDelay()======");

for (int i =1; i <=10; i++)

{

Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ======Await Delay====== ==>" + i);

await Task.Delay(1000);//异步延迟

}

Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ======End Delay()======");

Console.WriteLine();

});

Console.ReadKey();

}

5.CancellationToken 和 CancellationTokenSource 取消线程

5.1 CancellationToken

属性:

//表示当前CancellationToken是否可以被取消

public bool CanBeCanceled { get; }

//表示当前CancellationToken是否已经是取消状态

public bool IsCancellationRequested { get; }

方法:

//往CancellationToken中注册回调

public CancellationTokenRegistration Register(Action callback);

//当CancellationToken处于取消状态时,抛出System.OperationCanceledException异常

public void ThrowIfCancellationRequested();

5.2 CancellationTokenSource

属性:

//表示Token是否已处于取消状态

public bool IsCancellationRequested { get; }

//CancellationToken 对象

public CancellationToken Token { get; }

方法:

//立刻取消

public void Cancel();

//立刻取消

public void Cancel(bool throwOnFirstException);

//延迟指定时间后取消

public void CancelAfter(int millisecondsDelay);

//延迟指定时间后取消

public void CancelAfter(TimeSpan delay);

5.3 示例

CancellationTokenSource source = new CancellationTokenSource();

//注册一个线程取消后执行的逻辑

source.Token.Register(() =>

{

//这里执行线程被取消后的业务逻辑.

Console.WriteLine("-------------我是线程被取消后的业务逻辑---------------------");

});

Task.Run(() =>

{

while (!source.IsCancellationRequested)

{

Thread.Sleep(100);

Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);

}

}, source.Token);

Thread.Sleep(2000);

source.Cancel();

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

当前thread=4 正在运行

-------------我是线程被取消后的业务逻辑---------------------

当前thread=4 正在运行

6.asyncawait

async:

async 修饰符可将方法、lambda 表达式或匿名方法指定为异步。异步方法名字后习惯加个Async后缀async 关键字修饰的方法一般包含一个或多个await 表达式或语句,如果不包含 await 表达式或语句,则该方法将同步执行。 编译器警告将通知你不包含 await 语句的任何异步方法。async方法可以是下面三种返回类型:

TaskTask< TResult >void 这种返回类型一般用在event事件处理器中,或者用在你只需要任务执行,不关心任务执行结果的情况当中。任何其他具有GetAwaiter方法的类型(从C#7.0开始)

await:

await关键字只能在async 关键字修饰的方法(异步方法)中使用。await 运算符的操作数通常是以下其中一个 .NET 类型:Task、Task、ValueTask 或 ValueTask。 但是,任何可等待表达式都可以是 await 运算符的操作数。

示例:

无返回值:

static void Main(string[] args)

{

Console.WriteLine("主线程--开始");

var task = TestTaskAsync();

task.ContinueWith(t => Console.WriteLine("TestTaskAsync方法结束后执行"));

Console.WriteLine("主线程--结束");

Console.ReadKey();

}

private static async Task TestTaskAsync()

{

Console.WriteLine("开始执行TestTaskAsync方法");

Task task = new Task(() =>

{

Console.WriteLine("开始子线程耗时操作");

Thread.Sleep(4000);

Console.WriteLine("结束子线程耗时操作");

});

task.Start();

await task;

Console.WriteLine("await关键字后面的内容 1");

}

带返回值:

// 方法一:使用ContinueWith

Task<int> task = TestTaskIntAsync();

task.ContinueWith((t) =>

{

COnsole.WriteLine($"TestTaskIntAsync的返回值是:{ t.Result.ToString()}");

});

// 方法二:使用await

Task<int> task = TestTaskIntAsync();

int result = await task;

Console.WriteLine($"TestTaskIntAsync的返回值是:{ result }");

7.微软案例

以微软文档的做早餐的案例加以简化来讲解

1.同步执行

using System;

using System.Diagnostics;

using System.Threading;

using System.Threading.Tasks;

namespace ThreadTest

{

class Program

{

static void Main(string[] args)

{

Stopwatch stopwatch = new Stopwatch();

stopwatch.Start();

PourOJ();

PourCoffee();

ToastBread();

FryBacon();

FryEggs();

Console.WriteLine("早餐已经做完!");

stopwatch.Stop();

Console.WriteLine($"做早餐总计耗时:{ stopwatch.ElapsedMilliseconds}");

Console.ReadLine();

}

//倒橙汁

private static void PourOJ()

{

Thread.Sleep(1000);

Console.WriteLine("倒一杯橙汁");

}

//烤面包

private static void ToastBread()

{

Console.WriteLine("开始烤面包");

Thread.Sleep(3000);

Console.WriteLine("烤面包好了");

}

//煎培根

private static void FryBacon()

{

Console.WriteLine("开始煎培根");

Thread.Sleep(6000);

Console.WriteLine("培根煎好了");

}

//煎鸡蛋

private static void FryEggs()

{

Console.WriteLine("开始煎鸡蛋");

Thread.Sleep(6000);

Console.WriteLine("鸡蛋好了");

}

//倒咖啡

private static void PourCoffee()

{

Thread.Sleep(1000);

Console.WriteLine("倒咖啡");

}

}

}

在这里插入图片描述

2.并行执行

如果此时我们每一项任务都有一个单独的人去完成

那么可以如下:

<code>using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.Threading;

using System.Threading.Tasks;

namespace ThreadTest

{ -- -->

class Program

{

static void Main(string[] args)

{

Test();

Console.ReadLine();

}

private static void Test()

{

Stopwatch stopwatch = new Stopwatch();

stopwatch.Start();

List<Task> tasks = new List<Task>() { PourOJ(), ToastBread(), FryBacon(), FryEggs(), PourCoffee() };

Task.WhenAll(tasks).ContinueWith((t)=>

{

Console.WriteLine("早餐已经做完!");

stopwatch.Stop();

Console.WriteLine($"做早餐总计耗时:{ stopwatch.ElapsedMilliseconds}");

});

}

//倒橙汁

private static async Task PourOJ()

{

await Task.Delay(1000);

Console.WriteLine("倒一杯橙汁");

}

//烤面包

private static async Task ToastBread()

{

Console.WriteLine("开始烤面包");

await Task.Delay(3000);

Console.WriteLine("烤面包好了");

}

//煎培根

private static async Task FryBacon()

{

Console.WriteLine("开始煎培根");

await Task.Delay(6000);

Console.WriteLine("培根煎好了");

}

//煎鸡蛋

private static async Task FryEggs()

{

Console.WriteLine("开始煎鸡蛋");

await Task.Delay(6000);

Console.WriteLine("鸡蛋好了");

}

//倒咖啡

private static async Task PourCoffee()

{

await Task.Delay(1000);

Console.WriteLine("倒咖啡");

}

}

}

在这里插入图片描述

3.并行且可指定顺序执行

现在呢,有个问题,不可能每次做早餐你都有那么多帮手,同时帮你,如果现在要求,先倒橙汁,然后倒咖啡,其余的操作并行执行,应该如何操作呢?

只需将以上案例的Test 方法修改如下:

<code>private static async void Test()

{ -- -->

Stopwatch stopwatch = new Stopwatch();

stopwatch.Start();

await PourOJ();

await PourCoffee();

List<Task> tasks = new List<Task>() { ToastBread(), FryBacon(), FryEggs() };

await Task.WhenAll(tasks);

Console.WriteLine("早餐已经做完!");

stopwatch.Stop();

Console.WriteLine($"做早餐总计耗时:{ stopwatch.ElapsedMilliseconds}");

}

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。