C#并发编程——异步编程基础

  现代的异步.NET 程序使用两个关键字:async 和await。async 关键字加在方法声明上,它的主要目的是使方法内的await 关键字生效。如果async 方法有返回值,应返回Task<T>;如果没有返回值,应返回Task。这些task 类型相当于future,用来在异步方法结束时通知主程序。和其他方法一样,async 方法在开始时以同步方式执行。在async 方法内部,await 关键字对它的参数执行一个异步等待。它首先检查操作是否已经完成,如果完成了,就继续运行(同步方式)。否则,它会暂停async 方法,并返回,留下一个未完成的task。一段时间后,操作完成,async 方法就恢复运行。

  一个async 方法是由多个同步执行的程序块组成的,每个同步程序块之间由await 语句分隔。第一个同步程序块在调用这个方法的线程中运行,但其他同步程序块在哪里运行呢?情况比较复杂。
  最常见的情况是,用await 语句等待一个任务完成,当该方法在await 处暂停时,就可以捕捉上下文(context)。如果当前SynchronizationContext 不为空,这个上下文就是当前SynchronizationContext。如果当前SynchronizationContext 为空,则这个上下文为当前TaskScheduler。该方法会在这个上下文中继续运行。一般来说,运行UI 线程时采用UI 上下文,处理ASP.NET 请求时采用ASP.NET 请求上下文,其他很多情况下则采用线程池上下文。
  因此,在上面的代码中,每个同步程序块会试图在原始的上下文中恢复运行。如果在UI线程中调用DoSomethingAsync,这个方法的每个同步程序块都将在此UI 线程上运行。但是,如果在线程池线程中调用,每个同步程序块将在线程池线程上运行。
  要避免这种错误行为, 可以在await 中使用ConfigureAwait 方法, 将参数continueOnCapturedContext 设为false。接下来的代码刚开始会在调用的线程里运行,在被await 暂停后,则会在线程池线程里继续运行。

  不要用void 作为async 方法的返回类型! async 方法可以返回void,但是这仅限于编写事件处理程序。一个普通的async 方法如果没有返回值,要返回Task,而不是void。

  在编写任务并行程序时,要格外留意下闭包(closure)捕获的变量。记住闭包捕获的是引用(不是值),因此可以在结束时以不明显地方式分享这些变量。

  数据并行和任务并行都使用动态调整的分割器,把任务分割后分配给工作线程。线程池在需要的时候会增加线程数量。线程池线程使用工作窃取队列。

  每个.NET 程序都有一个线程池,线程池维护着一定数量的工作线程,这些线程等待着执行分配下来的任务。线程池可以随时监测线程的数量。

1、暂停一段时间

   Task 类有一个返回Task 对象的静态函数Delay,这个Task 对象会在指定的时间后完成。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace DemoTask
{
    class Program
    {
        static void Main(string[] args)
        {
            TimeSpan t = new TimeSpan(0, 0, 1);
            DelayResult(100, t);
            Console.ReadLine();
        }

        static async Task<int> DelayResult(int result, TimeSpan delay)
        {
            await Task.Delay(delay);
            Console.WriteLine($"threadId={Thread.CurrentThread.ManagedThreadId}");
            return result;
        }
    }
}
View Code

2、返回完成的任务

  可以使用Task.FromResult 方法创建并返回一个新的Task<T> 对象,这个Task 对象是已经完成的,并有指定的值。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace DemoTask
{
    class Program
    {
        static void Main(string[] args)
        {
            GetValueAsync();
            Console.ReadLine();
        }
        
        static Task<int> GetValueAsync()
        {
            return Task.FromResult(0);
        }
    }
}
View Code

3、报告进度

  使用IProgress<T> 和Progress<T> 类型,编写的async 方法需要有IProgress<T> 参数。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace DemoTask
{
    class Program
    {
        static void Main(string[] args)
        {
            CallMethodAsync();
            Console.ReadLine();
        }
        
        static async Task MethodAsync(IProgress<double> progress)
        {
            double percent = 0;
            await Task.Run(() =>
            {
                for(int i = 0; i < 10; i++)
                {
                    percent = i;
                    progress.Report(percent);
                    Thread.Sleep(1000);
                }
            });
        }

        static async Task CallMethodAsync()
        {
            var progress = new Progress<double>();
            progress.ProgressChanged += (sender, args) =>
            {
                Console.WriteLine($"进度:{args}");
            };
            await MethodAsync(progress);
        }
    }
}
View Code

  需要注意的是,IProgress<T>.Report 方法可以是异步的。这意味着真正报告进度之前,MethodAsync 方法会继续运行。基于这个原因,最好把T 定义为一个不可变类型,或者至少是值类型。如果T 是一个可变的引用类型,就必须在每次调用IProgress<T>.Report 时,创建一个单独的副本。

4、等待一组任务完成

  Task.WhenAll 方法可以实现这个功能。这个方法的输入为若干个任务,当所有任务都完成时,返回一个完成的Task 对象。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace DemoTask
{
    class Program
    {
        static void Main(string[] args)
        {
            FuncAsync();
            Console.ReadLine();
        }
        
        static async Task FuncAsync()
        {
            Task<int> task1 = Task.FromResult(1);
            Task<int> task2 = Task.FromResult(2);
            Task<int> task3 = Task.FromResult(3);
            int[] results = await Task.WhenAll(task1, task2, task3);
            foreach (var item in results)
                Console.WriteLine(item);
        }
    }
}
View Code

  如果有一个任务抛出异常,则Task.WhenAll 会出错,并把这个异常放在返回的Task 中。如果多个任务抛出异常,则这些异常都会放在返回的Task 中。但是,如果这个Task 在被await 调用,就只会抛出其中的一个异常。如果要得到每个异常,可以检查Task.WhenALl返回的Task 的Exception 属性。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace DemoTask
{
    class Program
    {
        static void Main(string[] args)
        {
            ObserveOneExceptionAsync();
            ObserveAllExceptionsAsync();
            Console.ReadLine();
        }
        
        static async Task ThrowNotImplementedExceptionAsync()
        {
            await Task.FromResult(0);
            throw new NotImplementedException("NotImplementedException");
        }

        static async Task ThrowInvalidOperationExceptionAsync()
        {
            await Task.FromResult(0);
            throw new InvalidOperationException("InvalidOperationException");
        }

        static async Task ObserveOneExceptionAsync()
        {
            var task1 = ThrowNotImplementedExceptionAsync();
            var task2 = ThrowInvalidOperationExceptionAsync();
            try
            {
                await Task.WhenAll(task1, task2);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            Console.WriteLine("----------------");
        }

        static async Task ObserveAllExceptionsAsync()
        {
            var task1 = ThrowNotImplementedExceptionAsync();
            var task2 = ThrowInvalidOperationExceptionAsync();
            Task task = Task.WhenAll(task1, task2);
            try
            {
                await task;
            }
            catch (Exception e)
            {
                AggregateException ae = task.Exception;
                foreach(var item in ae.InnerExceptions)
                    Console.WriteLine(item.Message);
            }
        }
    }
}
View Code

5、等待任意一个任务完成

   使用Task.WhenAny 方法。该方法的参数是一批任务,当其中任意一个任务完成时就会返回。作为返回值的Task 对象,就是那个完成的任务。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace DemoTask
{
    class Program
    {
        static void Main(string[] args)
        {
            RunAsync();
            Console.ReadLine();
        }
        
        static Task<int> Read()
        {
            return Task.FromResult(1);
        }

        static Task<int> Write()
        {
            return Task.FromResult(2);
        }

        static async Task RunAsync()
        {
            var t1 = Read();
            var t2 = Write();
            Task<int> task = await Task.WhenAny(t1, t2);
            Console.WriteLine(task.Result);
        }
    }
}
View Code

  Task.WhenAny 返回的task 对象永远不会以“故障”或“已取消”状态作为结束。该方法的运行结果总是一个Task 首先完成。如果这个任务完成时有异常,这个异常也不会传递给Task.WhenAny 返回的Task 对象。

  第一个任务完成后,考虑是否要取消剩下的任务。如果其他任务没有被取消,也没有被继续await,那它们就处于被遗弃的状态。被遗弃的任务会继续运行直到完成,它们的结果会被忽略,抛出的任何异常也会被忽略。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace DemoTask
{
    class Program
    {
        static void Main(string[] args)
        {
            ObserveOneExceptionAsync();
            Console.ReadLine();
        }

        static async Task ThrowNotImplementedExceptionAsync()
        {
            await Task.FromResult(1);
            throw new NotImplementedException("NotImplementedException");
        }

        static async Task ThrowInvalidOperationExceptionAsync()
        {
            await Task.FromResult(2);
            throw new InvalidOperationException("InvalidOperationException");
        }

        static async Task ObserveOneExceptionAsync()
        {
            var task1 = ThrowNotImplementedExceptionAsync();
            var task2 = ThrowInvalidOperationExceptionAsync();
            try
            {
                await Task.WhenAny(task1, task2);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            Console.WriteLine("没返回异常");
        }
    }
}
View Code

6、任务完成时的处理

  正在await 一批任务,希望在每个任务完成时对它做一些处理。另外,希望在任务一完成就立即进行处理,而不需要等待其他任务。

  最简单的方案是通过引入更高级的async 方法来await 任务,并对结果进行处理,从而重新构建代码。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace DemoTask
{
    class Program
    {
        static void Main(string[] args)
        {
            ProcessTasksAsync();
            Console.ReadLine();
        }

        static async Task<int> DelayAsync(int val)
        {
            await Task.Delay(TimeSpan.FromSeconds(val));
            return val;
        }

        static async Task AwaitAndProcessAsync(Task<int> t)
        {
            var result = await t;
            Console.WriteLine(result + "  " + DateTime.Now.Ticks / 1000 / 10000);
        }

        static async Task ProcessTasksAsync()
        {
            Task<int> t1 = DelayAsync(2);
            Task<int> t2 = DelayAsync(3);
            Task<int> t3 = DelayAsync(1);
            var tasks = new[] { t1, t2, t3 };
            Task[] ts = new Task[3];
            for (int i = 0; i < 3; i++)
                ts[i] = AwaitAndProcessAsync(tasks[i]);
            await Task.WhenAll(ts);
        }
    }
}
View Code

7、避免上下文延续

  在默认情况下,一个async 方法在被await 调用后恢复运行时,会在原来的上下文中运行。如果是UI 上下文,并且有大量的async 方法在UI 上下文中恢复,就会引起性能上的问题。

  为了避免在上下文中恢复运行,可让await 调用ConfigureAwait 方法的返回值,参数continueOnCapturedContext 设为false。

8、处理async Task方法的异常

9、处理async void方法的异常

原文地址:https://www.cnblogs.com/ACGame/p/11032211.html