c# 高级编程 15章316页 【异步编程】【基础】

246 阅读4分钟

观察线程和任务

  • 任务ID: Task.CurrentId
  • 线程ID: Thread.CurrentThread.ManagedThreadId
        public static void TraceThreadAndTask(string info)
        {
            string taskInfo = Task.CurrentId == null ? "no task" : "task " + Task.CurrentId;

            Console.WriteLine($"{info} in thread {Thread.CurrentThread.ManagedThreadId} and {taskInfo}");
        }

一个同步方法

        static string Greeting(string name)
        {
            TraceThreadAndTask($"running {nameof(Greeting)}");
            Task.Delay(3000).Wait();
            return $"Hello, {name}";
        }

将 同步方法 包装成 异步方法

  • 方法名后加上Async后缀
  • 返回一个任务
    • 此处返回一个返回string的任务Task<string>
    • 比较简单的做法是用Task.Run方法返回一个任务
      • 泛型版的Task.Run<string>创建一个返回字符串的任务
      • 由于编译器已经知道返回string,因此可以用Task.Run来简化
  • GreetingAsync方法在一个Task中运行
      static Task<string> GreetingAsync(string name) =>
            Task.Run(() =>
            {
                TraceThreadAndTask($"running {nameof(GreetingAsync)}");
                return Greeting(name);
            });

调用异步方法

这里,异步方法结束后,不会回到调用线程

        private static async void CallerWithAsync()
        {
            TraceThreadAndTask($"started {nameof(CallerWithAsync)}");
            string result = await GreetingAsync("Stephanie");
            Console.WriteLine(result);
            TraceThreadAndTask($"ended {nameof(CallerWithAsync)}");
        }

        private static async void CallerWithAsync2()
        {
            TraceThreadAndTask($"started {nameof(CallerWithAsync2)}");
            Console.WriteLine(await GreetingAsync("Stephanie"));
            TraceThreadAndTask($"ended {nameof(CallerWithAsync2)}");
        }

输出:

started CallerWithAsync in thread 1 and no task
running GreetingAsync in thread 3 and task 1
running Greeting in thread 3 and task 1
Hello, Stephanie
//最终没有回到thread1,而是仍然在thread3
ended CallerWithAsync in thread 3 and no task

async 和 await 关键字

  • async可以放置在如下方法之前:
  1. 此方法返回值为void
  2. 此方法返回一个类型,这个类型有GetAwaiter()方法
  • await 可以等待任何提供GetAwaiter()方法的对象

应避免给返回void的方法使用async修饰符

使用 Awaiter

  • Task类的GetAwaiter()返回一个TaskAwaiter
  • TaskAwaiterOnCompleted()方法,可以指定Task完成时需要做的事情
  • 这里,用本地函数OnCompleteAwaiter()来实现Task完成时需要做的事情

这里使用TaskAwaiter,异步方法结束后,不会回到调用线程

        private static void CallerWithAwaiter()
        {
            TraceThreadAndTask($"starting {nameof(CallerWithAwaiter)}");
            TaskAwaiter<string> awaiter = GreetingAsync("Matthias").GetAwaiter();
            awaiter.OnCompleted(OnCompleteAwaiter);

            void OnCompleteAwaiter()
            {
                Console.WriteLine(awaiter.GetResult());
                TraceThreadAndTask($"ended {nameof(CallerWithAwaiter)}");
            }
        }

输出:

starting CallerWithAwaiter in thread 1 and no task
running GreetingAsync in thread 3 and task 1
running Greeting in thread 3 and task 1
Hello, Matthias
ended CallerWithAwaiter in thread 3 and no task

延续任务

  • Task类的ContinueWith()方法定义了任务完成后要调用的代码
  • ContinueWith()方法,入参是一个委托
  • 这个委托,会将已完成的Task作为参数传入
  • 使用已完成TaskResult属性,可以访问Task返回的结果
        private static void CallerWithContinuationTask()
        {
            TraceThreadAndTask($"started {nameof(CallerWithContinuationTask)}");

            var t1 = GreetingAsync("Stephanie");

            t1.ContinueWith(t =>
            {
                string result = t.Result;
                Console.WriteLine(result);

                TraceThreadAndTask($"ended {nameof(CallerWithContinuationTask)}");
            });
        }

输出:

started CallerWithContinuationTask in thread 1 and no task
running GreetingAsync in thread 3 and task 1
running Greeting in thread 3 and task 1
Hello, Stephanie
ended CallerWithContinuationTask in thread 4 and task 2

这里使用Continuation,后续任务会在另一个线程上,并且不会回到调用线程

同步上下文

  • 前例中,在方法的不同声明阶段,使用了不同的线程
    • 对于一个控制台引用程序,这通常没什么问题。
      • 但也必须保证,在所有该完成的任务完成之前,至少有一个前台线程仍然在运行
      • 可以通过Console.ReadLine()保证主线程一直在运行
    • 为了执行某些动作,会需要绑定到指定线程上。例如WPF或Windows应用程序中,只有UI线程才能访问UI元素。这将会是一个问题。

如果使用asyncawait关键字,当await完成之后,不需要进行任何特别处理,就能访问UI线程

如果调用异步方法的线程,分配给了同步上下文,那么await完成之后,将继续执行。默认情况下,就是如此,使用了同步上下文。

默认情况下,会把线程,转换到,拥有同步上下文的线程中


  • 同步上下文
    • WPF应用程序,有DispatcherSynchronizationContext属性
    • Windows Forms应用程序,有WindowsFormsSynchronationContext属性
    • Windows应用程序,有WinRTSynchronationContext属性

  • 有些情况下,例如:await后面没有用到任何UI元素,这时不按默认行为去切换到同步上下文,反而会提高性能,执行得更加快。因此

如果不需要使用 相同的 同步上下文,则必须显式调用Task的方法ConfigureAwait(continueOnCapturedContext: false)

使用多个异步方法

  • 第二个异步方法 需要等 第一个异步方法 执行完再执行 的实现方式:
        private static async void MultipleAsyncMethods()
        {
            string s1 = await GreetingAsync("Stephanie");
            string s2 = await GreetingAsync("Matthias");
            Console.WriteLine($"Finished both methods.{Environment.NewLine} Result 1: {s1}{Environment.NewLine} Result 2: {s2}");
        }
  • 两个异步方法并行执行,等它们都执行完再继续往下执行 的实现方式:

一个组合器:可以接受多个同一类型的参数,并返回同一类型的值。多个同一类型的参数,被组合成一个参数来传递

Task组合器,接受多个Task对象作为参数,并返回一个Task

  • Task类定义了WhenAll()WhenAny()组合器
        private static async void MultipleAsyncMethodsWithCombinators1()
        {
            Task<string> t1 = GreetingAsync("Stephanie");
            Task<string> t2 = GreetingAsync("Matthias");
            await Task.WhenAll(t1, t2);
            Console.WriteLine($"Finished both methods.{Environment.NewLine} Result 1: {t1.Result}{Environment.NewLine} Result 2: {t2.Result}");
        }  
  • Task类的WhenAll()中,如果所有任务返回相同的类型string, 则用数组string[]来存放await返回的结果
        private static async void MultipleAsyncMethodsWithCombinators2()
        {
            Task<string> t1 = GreetingAsync("Stephanie");
            Task<string> t2 = GreetingAsync("Matthias");
            string[] result = await Task.WhenAll(t1, t2);
            Console.WriteLine($"Finished both methods.{Environment.NewLine} Result 1: {result[0]}{Environment.NewLine} Result 2: {result[1]}");
        }

ValueTask: 一种可以用于 等待 的新类型

  • ValueTask是一个结构
    • 一般来说Task在堆上的开销是可以忽略的,因此ValueTask作为结构类型,在堆上开销的优势也不是很大。但也不绝对。
        static async ValueTask<string> GreetingValueTaskAsync(string name)
        {
            if (names.TryGetValue(name, out string result))
            {
                return result;
            }
            else
            {
                result = await GreetingAsync(name);
                names.Add(name, result);
                return result;                
            }
        }

下面的例子中,没有使用asyncawait:

        static ValueTask<string> GreetingValueTask2Async(string name)
        {
            if (names.TryGetValue(name, out string result))
            {
                return new ValueTask<string>(result);
            }
            else
            {
                Task<string> t1 =  GreetingAsync(name);
                
                TaskAwaiter<string> awaiter = t1.GetAwaiter();
                awaiter.OnCompleted(OnCompletion);
                return new ValueTask<string>(t1);

                void OnCompletion()
                {
                    names.Add(name, awaiter.GetResult());
                }
            }
        }

转换异步模式

并非.NET Framework中的所有类 都引入了 基于任务的异步模式 的方法,许多类只提供了BeginXXX方法和EndXXX方法。但是可以将后者转换成前者

  • 可以使用Task.Factory.FromAsync()方法
    • 第一个参数:IAsyncResult类型
    • 第二个参数:一个委托,这个委托的
      • 入参是IAsyncResult
      • 返回类型TFromAsync<T>()泛型方法指定
        private static async void ConvertingAsyncPattern()
        {
            HttpWebRequest request = WebRequest.Create("http://www.microsoft.com") as HttpWebRequest;

            using (WebResponse response = await Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse(null, null), request.EndGetResponse))
            {
                Stream stream = response.GetResponseStream();
                using (var reader = new StreamReader(stream))
                {
                    string content = reader.ReadToEnd();
                    Console.WriteLine(content.Substring(0, 100));
                }
            }
        }

警告:在旧应用程序中,通常在使用最古早的异步模式时,使用委托的BeginInvoke()方法。在.NET Core应用程序中使用BeginInvoke()时,编译器不会报错。但是,在运行时,将抛出一个平台不支持的异常

命令行启动.NET Core的dll文件,并给定命令行参数

> dotnet Foundations.dll -async