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、处理同步阻塞

原文地址:https://www.cnblogs.com/netlock/p/14061022.html