zoukankan      html  css  js  c++  java
  • C#中 Thread,Task,Async/Await 异步编程

    什么是异步

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

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

  • 相关阅读:
    3.13 获取位置
    团队博客(三)
    团队博客(二)
    团队博客(一)
    Android抽奖
    声网实现视频会议(二)
    声网实现视频会议(一)
    Android弹幕实现原理
    人月神话 胸有成竹
    Android的弹幕功能实现(四)
  • 原文地址:https://www.cnblogs.com/netlock/p/14061022.html
Copyright © 2011-2022 走看看