什么是异步
同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。
异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。本篇主要介绍Task、async/await相关的内容
简单来说:就是使用同步方法时,线程会被耗时操作一直占有,直到耗时操作完成。而使用异步方法,程序走到await关键字时会立即return,释放线程,余下的代码会放进一个回调中(Task.GetAwaiter()的UnsafeOnCompleted(Action)回调),耗时操作完成时才会回调执行,所以async/await是语法糖,其本质是一个状态机。
那是不是所有的action都要用async/await呢?
不是。一般的磁盘IO或者网络请求等耗时操作才考虑使用异步,不要为了异步而异步,异步也是需要消耗性能的,使用不合理会适得其反。
参考为什么使用async https://www.cnblogs.com/xhznl/p/13064731.htm
async/await异步编程不能提升响应速度,但是可以提升响应能力(吞吐量)。异步和同步各有优劣,要合理选择,不要为了异步而异步。
1.线程(Thread)
多线程的意义在于一个应用程序中,有多个执行部分可以同时执行;对于比较耗时的操作(例如io,数据库操作),或者等待响应(如WCF通信)的操作,可以单独开启后台线程来执行,这样主线程就不会阻塞,可以继续往下执行;等到后台线程执行完毕,再通知主线程,然后做出对应操作!
在C#中开启新线程比较简单
static void Main(string[] args) { Console.WriteLine("主线程开始"); //IsBackground=true,将其设置为后台线程 Thread t = new Thread(Run) { IsBackground = true }; t.Start(); Console.WriteLine("主线程在做其他的事!"); //主线程结束,后台线程会自动结束,不管有没有执行完成 //Thread.Sleep(300); Thread.Sleep(1500); Console.WriteLine("主线程结束"); } static void Run() { Thread.Sleep(700); Console.WriteLine("这是后台线程调用"); }
Thread t = new Thread(()=> { Console.WriteLine("Starting..."); for (int i = 0; i < 10; i++) { Console.WriteLine(i); } }); t.Start(); static void Main(string[] args) { int b = 10; string c = "主线程"; Thread t = new Thread(()=> PrintNumbers(b,c)); t.Start(); } static void PrintNumbers(int count,string name) { for (int i = 0; i < count; i++) { Console.WriteLine("name:{0},i:{1}",name,i); } }
1.1 线程池
试想一下,如果有大量的任务需要处理,例如网站后台对于HTTP请求的处理,那是不是要对每一个请求创建一个后台线程呢?显然不合适,这会占用大量内存,而且频繁地创建的过程也会严重影响速度,那怎么办呢?线程池就是为了解决这一问题,把创建的线程存起来,形成一个线程池(里面有多个线程),当要处理任务时,若线程池中有空闲线程(前一个任务执行完成后,线程不会被回收,会被设置为空闲状态),则直接调用线程池中的线程执行(例asp.net处理机制中的Application对象),
ThreadPool相对于Thread来说可以减少线程的创建,有效减小系统开销;但是ThreadPool不能控制线程的执行顺序,我们也不能获取线程池内线程取消/异常/完成的通知,即我们不能有效监控和控制线程池中的线程
使用事例:
for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(m => { Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString()); }); } Console.Read();
class Program { static void Main(string[] args) { WaitCallback wc1 = s =>{ Console.WriteLine("线程ID:{0},开始执行", Thread.CurrentThread.ManagedThreadId); Stopwatch stw = new Stopwatch(); stw.Start(); long result = SumNumbers(10000000); stw.Stop(); Console.WriteLine("线程ID:{0},执行完成,执行结果:{1},执行用时{2},",Thread.CurrentThread.ManagedThreadId,result,stw.ElapsedMilliseconds); }; WaitCallback wc2 = s => { Console.WriteLine("线程ID:{0},开始执行", Thread.CurrentThread.ManagedThreadId); Stopwatch stw = new Stopwatch(); stw.Start(); long result = SumNumbers(10000000); stw.Stop(); Console.WriteLine("线程ID:{0},执行完成,执行结果:{1},执行用时{2},", Thread.CurrentThread.ManagedThreadId, result, stw.ElapsedMilliseconds); }; WaitCallback wc3 = s => { Console.WriteLine("线程ID:{0},开始执行", Thread.CurrentThread.ManagedThreadId); Stopwatch stw = new Stopwatch(); stw.Start(); long result = SumNumbers(10000000); stw.Stop(); Console.WriteLine("线程ID:{0},执行完成,执行结果:{1},执行用时{2},", Thread.CurrentThread.ManagedThreadId, result, stw.ElapsedMilliseconds); }; ThreadPool.QueueUserWorkItem(wc1); ThreadPool.QueueUserWorkItem(wc2); ThreadPool.QueueUserWorkItem(wc3); Console.ReadKey(); } static long SumNumbers(int count) { long sum = 0; for (int i = 0; i < count; i++) { sum += i; } Thread.Sleep(1000); return sum; } } //等待线程池的线程执行 class Program { static void Main(string[] args) { using (ManualResetEvent m1 = new ManualResetEvent(false)) using (ManualResetEvent m2 = new ManualResetEvent(false)) using (ManualResetEvent m3 = new ManualResetEvent(false)) { ThreadPool.QueueUserWorkItem( s =>{ Console.WriteLine("线程ID:{0},开始执行", Thread.CurrentThread.ManagedThreadId); Stopwatch stw = new Stopwatch(); stw.Start(); long result = SumNumbers(10000000); stw.Stop(); m1.Set(); Console.WriteLine("线程ID:{0},执行完成,执行结果:{1},执行用时{2},", Thread.CurrentThread.ManagedThreadId, result, stw.ElapsedMilliseconds); }); ThreadPool.QueueUserWorkItem( s =>{ Console.WriteLine("线程ID:{0},开始执行", Thread.CurrentThread.ManagedThreadId); Stopwatch stw = new Stopwatch(); stw.Start(); long result = SumNumbers(10000000); stw.Stop(); m2.Set(); Console.WriteLine("线程ID:{0},执行完成,执行结果:{1},执行用时{2},", Thread.CurrentThread.ManagedThreadId, result, stw.ElapsedMilliseconds); }); ThreadPool.QueueUserWorkItem( s =>{ Console.WriteLine("线程ID:{0},开始执行", Thread.CurrentThread.ManagedThreadId); Stopwatch stw = new Stopwatch(); stw.Start(); long result = SumNumbers(10000000); stw.Stop(); m3.Set(); Console.WriteLine("线程ID:{0},执行完成,执行结果:{1},执行用时{2},", Thread.CurrentThread.ManagedThreadId, result, stw.ElapsedMilliseconds); }); //等待线程池的线程执行 m1.WaitOne(); m2.WaitOne(); m3.WaitOne(); Console.WriteLine("所有线程执行完成"); } Console.ReadKey(); } static long SumNumbers(int count) { long sum = 0; for (int i = 0; i < count; i++) { sum += i; } Thread.Sleep(3000); return sum; } }
1.2 信号量(Semaphore)
Semaphore负责协调线程,可以限制对某一资源访问的线程数量
这里对SemaphoreSlim类的用法做一个简单的事例:
static SemaphoreSlim semLim = new SemaphoreSlim(3); //3表示最多只能有三个线程同时访问 static void Main(string[] args) { for (int i = 0; i < 10; i++) { new Thread(SemaphoreTest).Start(); } Console.Read(); } static void SemaphoreTest() { semLim.Wait(); Console.WriteLine("线程" + Thread.CurrentThread.ManagedThreadId.ToString() + "开始执行"); Thread.Sleep(2000); Console.WriteLine("线程" + Thread.CurrentThread.ManagedThreadId.ToString() + "执行完毕"); semLim.Release(); }
2.Task
Task是在ThreadPool的基础上推出的,我们简单了解下ThreadPool。ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少线程的创建,节省开销
Task是.NET4.0加入的,跟线程池ThreadPool的功能类似,用Task开启新任务时,会从线程池中调用线程,而Thread每次实例化都会创建一个新的线程。
Task,对ThreadPool和Thread的包装,可以根据任务时间长短选择使用线程池还是新的线程,通过进一步扩展,增加了返回值、多个线程并行/串行等功能。它的核心是一个调度器,默认是ThreadPoolTaskScheduler。Task使用的是异步操作一个线程池线程
Task和thread很大的一个区别就是,在task中如果有一个阻塞的话,整个task就会被阻塞住,当前的线程ID不会改变,在thread中如果有一个阻塞的话,会去执行另外的thread,然后回来执行原来的那个thread,线程ID会改变为其他的ID。 能用Task就用Task,底下都是用的Thread或者ThreadPool
/// <summary> /// 最简单的使用方式 /// </summary> /// <returns></returns> [HttpGet] [Route("GetTask")] public IActionResult GetTask() { Console.ForegroundColor = ConsoleColor.Red; // 执行一个无返回值的任务 Task.Run(() => { Console.WriteLine("runing ..."); }); // 执行一个返回 int 类型结果的任务 var res1 = Task.Run<int>(() => { return 483; }); // 声明一个任务,仅声明,不执行 Task t = new Task(() => { Console.WriteLine("声明"); }); Console.ResetColor(); return Ok("test"); } /// <summary> /// 使用 TaskFactory 工厂开始异步任务 ///使用 TaskFactory 创建并运行了两个异步任务,同时把这两个任务加入了任务列表 tasks 中 ///然后立即迭代此 tasks 获取异步任务的执行结果,使用 TaskFactory 工厂类,可以创建一组人物,然后依次执行它们 /// </summary> /// <returns></returns> [HttpGet] [Route("GetTask2")] public IActionResult GetTask2() { Console.ForegroundColor = ConsoleColor.Red; List<Task<int>> tasks = new List<Task<int>>(); TaskFactory factory = new TaskFactory(); tasks.Add(factory.StartNew<int>(() => { Console.WriteLine("t1"); return 1; })); tasks.Add(factory.StartNew<int>(() => { Console.WriteLine("t2"); return 2; })); tasks.Add(factory.StartNew<int>(() => { Console.WriteLine("t3"); return 3; })); tasks.ForEach(t => Console.WriteLine("Task:{0}", t.Result)); Console.ResetColor(); return Ok("test2"); } /// <summary> /// 处理 Task 中的异常 ///异步任务中发生异常会导致任务抛出 TaskCancelException 的异常,仅表示任务退出,程序应当捕获该异常;然后,立即调用 Task 进行状态判断,获取内部异常 上面的代码模拟了 Task 内部发生的异常,并捕获了异常 ///通常情况下,推荐使用 Task 的任务状态判断以进行下一步的任务处理(如果需要),如果仅仅是简单的执行一个异步任务,直接捕获异常即可,这里使用了状态判断,如果任务已完成,则打印一则消息:IsCompleted;很明显,在上面的代码中,此 “IsCompleted” 消息并不会被打印到控制台 ///注意,这里使用了 task.IsCompletedSuccessfully 而不是 task.IsCompleted,这两者的区别在于,前者只有在任务正常执行完成,无异常,无中途退出指令的情况下才会表示已完成,而 task.IsCompleted 则仅仅表示“任务完成” /// </summary> /// <returns></returns> [HttpGet] [Route("GetTask3")] public IActionResult GetTask3() { Console.ForegroundColor = ConsoleColor.Red; var task = Task.Run(() => { Console.WriteLine("SimpleTask"); Task.Delay(1000).Wait(); throw new Exception("SimpleTask Error"); }); try { task.Wait(); } catch (Exception ex) { Console.WriteLine(ex.Message); } if (task.IsCompletedSuccessfully)//任务成功 { Console.WriteLine("IsCompletedSuccessfully"); } if (task.IsCompleted)//任务完成 { Console.WriteLine("IsCompleted"); } Console.ResetColor(); return Ok("test2"); }
2.1 Task<TResult>
Task<TResult>就是有返回值的Task,TResult就是返回值类型。
Console.WriteLine("主线程开始"); //返回值类型为string Task<string> task = Task<string>.Run(() => { Thread.Sleep(2000); return Thread.CurrentThread.ManagedThreadId.ToString(); }); //会等到task执行完毕才会输出; Console.WriteLine(task.Result); Console.WriteLine("主线程结束");
2.2 Task 阻塞
Thread的Join方法可以阻塞调用线程,但是有一些弊端:①如果我们要实现很多线程的阻塞时,每个线程都要调用一次Join方法;②如果我们想让所有的线程执行完毕(或者任一线程执行完毕)时,立即解除阻塞,使用Join方法不容易实现。Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制线程阻塞。
task.Wait() 表示等待task执行完毕,功能类似于thead.Join(); Task.WaitAll(Task[] tasks) 表示只有所有的task都执行完成了再解除阻塞;Task.WaitAny(Task[] tasks)表示只要有一个task执行完毕就解除阻塞
2.3 Task 并行任务
Task并行await(Task.WhenAll) Task.WhenAll 的意思是将所有等待的异步操作同时执行 (不要用在dbcontext 下面方法已经报错了 因为同一个DbContext不支持并发)
var count = source.CountAsync(); var items = source.Skip((pageindex - 1) * pagesize).Take(pagesize).ToListAsync(); //Task并行 多个任务同时运行 注意ef的dbcontext很狗血 await Task.WhenAll(count, items).ConfigureAwait(false); //ConfigureAwait(false)防止死锁
3. async/await
async用来修饰方法,表明这个方法是异步的,声明的方法的返回类型必须为:void,Task或Task<TResult>。
await必须用来修饰Task或Task<TResult>,而且只能出现在已经用async关键字修饰的异步方法中。通常情况下,async/await成对出现才有意义,
static void Main(string[] args) { Console.WriteLine("-------主线程启动-------"); Task<int> task = GetStrLengthAsync(); Console.WriteLine("主线程继续执行"); Console.WriteLine("Task返回的值" + task.Result); Console.WriteLine("-------主线程结束-------"); } static async Task<int> GetStrLengthAsync() { Console.WriteLine("GetStrLengthAsync方法开始执行"); //此处返回的<string>中的字符串类型,而不是Task<string> string str = await GetString(); Console.WriteLine("GetStrLengthAsync方法执行结束"); return str.Length; } static Task<string> GetString() { //Console.WriteLine("GetString方法开始执行") return Task<string>.Run(() => { Thread.Sleep(2000); return "GetString的返回值"; }); }
task.wait可以让主线程等待后台线程执行完毕,await和wait类似,同样是等待,等待Task<string>.Run()开始的后台线程执行完毕,不同的是await不会阻塞主线程,只会让GetStrLengthAsync方法暂停执行
4.异步的回调
文中所有Task<TResult>的返回值都是直接用task.result获取,这样如果后台任务没有执行完毕的话,主线程会等待其执行完毕,这样的话就和同步一样了(看上去一样,但其实await的时候并不会造成线程的阻塞,web程序感觉不到,但是wpf,winform这样的桌面程序若不使用异步,会造成UI线程的阻塞)。简单演示一下Task回调函数的使用:
Console.WriteLine("主线程开始"); Task<string> task = Task<string>.Run(() => { Thread.Sleep(2000); return Thread.CurrentThread.ManagedThreadId.ToString(); }); //会等到任务执行完之后执行 task.GetAwaiter().OnCompleted(() => { Console.WriteLine(task.Result); }); Console.WriteLine("主线程结束"); Console.Read();
OnCompleted中的代码会在任务执行完成之后执行!
另外task.ContinueWith()也是一个重要的方法:
Console.WriteLine("主线程开始"); Task<string> task = Task<string>.Run(() => { Thread.Sleep(2000); return Thread.CurrentThread.ManagedThreadId.ToString(); }); task.GetAwaiter().OnCompleted(() => { Console.WriteLine(task.Result); }); task.ContinueWith(m=>{Console.WriteLine("第一个任务结束啦!我是第二个任务");}); Console.WriteLine("主线程结束"); Console.Read();
ContinueWith()方法可以让该后台线程继续执行新的任务。
C#中ValueTask<T>与Task<T>的区别是什么?
ValueTask是对Task的包装。因为是包装的原因,所以您可将所有用Task的地方转换为ValueTask,编译器并不会报错,而且ValueTask还可以转换为Task
ValueTask会避免同步情况下一些不必要的内存分配,
ValueTask出现的目的是为了提升性能,而被提升的对象就是Task,一个Task可以await多次就不能使用ValueTask,执行一个异步操作然后await得到的Task只有一个await可以用。在这种用法下,我们不需要:多次await Task、处理并发地await、处理同步阻塞