(十)异步-什么是异步方法(2)

一、什么是异步方法

异步方法在完成其工作之前即返回到调用方法,然后在调用方法继续执行的时候完成其工作。

特点:

  • 方法头中包含 async 方法修饰符。
  • 包含一个或多个 await 表达式,表示可以异步完成的任务。
  • 必须具备3种返回类型之一:
    • void
    • Task
    • Task<T>
    • ValueTask<T>
  • 任何具有公开可访问的 GetAwaiter 方法的类型。
  • 异步方法的形参可以为任意类型、任意数量,但不能为 out 或 ref 参数。
  • 按照约定,异步方法的名称应该以 Async 为后缀。
  • 除了方法以外, Lambda 表达式和匿名方法也可以作为异步对象。
async Task<int> CountCharactersAsync(int id,string site)
{
Console.WriteLine("Starting CountCharacters");
WebClient wc = new WebClient();
//await 表达式
string result = await wc.DownloadStringTaskAsync( new Uri(site) );
//Lambda 表达式
// var result = await Task.Run(() => wc.DownloadString(new Uri(site)));
return result = Length;
}

async 关键字

  • 异步方法的方法头必须包含 async 关键字,且必须位于返回类型之前。
  • 该修饰符只是标识该方法包含一个或多个 await 表达式。它本身并不能创建任何异步操作。
  • async 关键字是一个上下文关键字。除了作为方法修饰符(或 Lambda 表达式修饰符、匿名方法修饰符)之外,async 还可用作标识符。(比如 int async = 0;)

返回类型:

  • 1)Task: 如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,那么异步方法可以返回一个 Task 类型的对象。如果异步方法中包含任何 return 语句,则它们不能返回任何东西:
Task someTask = DoStuff.CalculateSumAsync(5,6);
...
someTask.Wait();
  • 2)ask<T>: 如果调用方法要从调用中获取一个 T 类型的值,异步方法的返回类型就必须是Task<T>。调用方法将通过读取 Task 的 Result 属性来获取整个 T 类型的值。
Task<T> value = DoStuff.CalcuateSumAsync(5,6);
...
Console.WriteLine($"Value:{ value.Result }");
  • 3)ValueTask<T>: 这是一个值类型对象,它与 Task<Tgt; 类似,但用于任务结果可能已经可用的情况。因为它是一个值类型,所以它可以放在栈上。,而无须像 Task<T> 对象那样在堆上分配空间。
    class Program
    {
        static void Main(string[] args)
        {
            ValueTask<int> value = DoAsynStuff.CalculateSumAsync(0, 6);
            //处理其他事情
            Console.WriteLine($"Value:{ value.Result }");
            value = DoAsynStuff.CalculateSumAsync(5, 6);
            //处理其他事情
            Console.WriteLine($"Value:{ value.Result }");
            Console.ReadKey();
        }

        static class DoAsynStuff
        {
            public static async ValueTask<int> CalculateSumAsync(int i1,int i2)
            {
                // 如i1 == 0,则可以避免执行长时间运行的任务
                if(i1 == 0)
                {
                    return i2; 
                }

                int sum = await Task<int>.Run(() => GetSum(i1, i2));
                return sum;
            }

            private static int GetSum(int i1,int i2){  return i1 + i2; }
        }

    }

我的编程环境不支持ValueTask<T>,查了相关资料可知,可能.NET 5.0 版本及以上才支持,而我电脑里最高版本才 4.6。算了,我就不测试了,先把例子贴上去,以后有条件了(不想再花时间安装 .NET 框架)再研究。

  • 4)void: 如果调用方法仅仅想执行异步方法,而不需要与它做任何进一步的交互时,异步方法可以返回 void 类型。如果异步方法中包含任何 return 语句,则它们不能返回任何东西。(这跟返回类型 Task 一样,包含 await时,等待任务完成之前,还可以执行下一个代码块,否则,等待任务完成才能执行下一步)

void 和 Task 的区别代码例子:


   class MyDownloadString
    {
        const string Str = "https://www.baidu.com/";
        Stopwatch sw = new Stopwatch();

        public async void DoRun()
        {
            sw.Start();

            await returnTaskValue();
            Console.WriteLine("----------:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
            returnVoidValue();
            Console.WriteLine("++++++++++:{0,4:N0}", sw.Elapsed.TotalMilliseconds);

            Thread.Sleep(500);
            Console.WriteLine();
            Console.WriteLine("暂停线程,为的是测试Task<string>,跟 Task 和 void 的无关:");

            Task<string> t = CountCharacters("Task<string>");
            Console.WriteLine("**********:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
            //总结:
            //1、如果 Task 和 void 异步方法里有 await,则方法内需要等待完成才能返回
            //假设 Task 和 void 异步方法内有 await:
            //2、在同层级方法内调用异步方法,Task 方法 添加 await 等待任务完成才能执行下一个语句
            //void 方法并不会等待任务完成,而继续执行下一个语句

            //3、 Task<string> 在同一层方法被调用,则执行异步处理。跟 void 一样。

            //4、同一层方法作为调用者,如果被调用者为异步方法时:
            //执行 void 和 Task<string>方法,不需要 await,则执行异步处理。
            //执行 Task 方法 需要 await 执行等待任务完成,才能执行下一个语句;否则,就异步处理。

        }

        private async Task returnTaskValue()
        {
            Console.WriteLine("Task_Start:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
            var result = await Task.Run(() => CountCharacters("Task"));
            Console.WriteLine("Task_End:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
        }

        private async void returnVoidValue()
        {
            Console.WriteLine("Void_Start:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
            var result = await Task.Run(() => CountCharacters("Void"));
            Console.WriteLine("Void_End:{0,4:N0}", sw.Elapsed.TotalMilliseconds);
        }

        private async Task<string> CountCharacters(string strId)
        {
            WebClient wc1 = new WebClient();
            Console.WriteLine("{0}_DownloadBefore:{1,4:N0}", strId, sw.Elapsed.TotalMilliseconds);
            var result = await wc1.DownloadStringTaskAsync(new Uri("https://www.baidu.com/"));
            Console.WriteLine("{0}_DownloadAfter:{1,4:N0}", strId, sw.Elapsed.TotalMilliseconds);
            return result;
        }
    }

输出结果:

>Task_Start:   1
Task_DownloadBefore:  68
Task_DownloadAfter: 420
Task_End: 435
----------: 435
Void_Start: 436
Void_DownloadBefore: 437
++++++++++: 440
Void_DownloadAfter: 469
Void_End: 469

暂停线程,为的是测试Task<string>,跟 Task 和 void 的无关:
Task<string>_DownloadBefore: 950
**********: 952
Task<string>_DownloadAfter: 983

void 返回类型使用“调用并忘记”的异步方法:

 class Program
    {
        static void Main(string[] args)
        {
            DoAsynStuff.CalculateSumAsync(5, 6);
            //如果参数值为1,则先等待void 方法执行完成之后才能执行下一个语句
            //这样做法会有安全隐患,适合 void 异步方法处理时间短,Sleep 参数值设置足够大
            Thread.Sleep(200);
            Console.WriteLine("Program Exiting");
            Console.ReadKey();
        }

        static class DoAsynStuff
        {
            public static async void CalculateSumAsync(int i1,int i2)
            {
                int value = await Task.Run(() => GetSum(i1, i2));
                Console.WriteLine("Value:{0}", value);
            }

            private static int GetSum(int i1,int i2)
            {
                return i1 + i2;
            }
        }

    }
  • 任何具有可访问的 GetAwaiter 方法的类型。

二、异步方法的控制流

异步方法的结构包含三个不同区域:

  • await 表达式之前的部分
  • await 表达式:异步执行的任务
  • await 表达式之后的部分

图-阐明了一个异步方法的控制流

请添加图片描述

目前有两个控制流: 一个在异步方法内,一个在调用方法内。
当后续部分遇到 retun 语句或到达方法末尾时:

  • 如果方法的返回类型为 void,控制流将退出。
  • 如果方法的返回类型为 Task,则后续部分设置 Task 的状态属性并退出。如果返回类型为
    Task<T>ValueTask<T>,则后续部分还将设置对象的 Result 属性。

1、await 表达式

await 表达式指定了一个异步执行的任务。

await task //task:空闲对象

一个空闲对象即是一个 awaitable 类型的实例。 awaitable 类型是指包含 GetAwaiter 方法的类型,该方法没有参数,返回一个 awaiter 类型的对象。

awaiter 类型包含以下成员:

bool IsCompleted{ get; }
void OnCompleted(Action);

void GetResult();
T GetResult();

2、关于 Task.Run:

使用 Task.Run 方法来创建一个 Task。

//是以 Func<TReturn> 委托为参数
public static Task Run(Func<TReturn> function);
//还有其他重载的类型
...

创建委托三种实现方式:

    class MyClass
    {
        public int Get10()
        {
            return 10;
        }

        public async Task DoWorkAsync()
        {
            Func<int> ten = new Func<int>(Get10);
            int a = await Task.Run(ten);
            int b = await Task.Run(new Func<int>(Get10));
            int c = await Task.Run(()=> { return 10; });

            Console.WriteLine($"{ a } { b } { c }");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Task t = (new MyClass()).DoWorkAsync();
            t.Wait();
            Console.ReadKey();
        }

    }

输出结果:

10 10 10

表-Task.Run 重载的返回类型和签名

返回类型 签名
Task Run( Action action)
Task Run( Action action, CancellationToken token )
Task<TResult> Run( Func<TResult> function )
Task<TResult> Run( Func<TResult> function, CancellationToken token)
Task Run( Func<Task> function )
Task Run( Func<Task> function, CancellationToken token )
Task<TResult> Run( Func<Task<TResult>> function )
Task<TResult> Run( Func<Task<TResult>> function, CancellationToken token )

表-可作为 Task.Run 方法第一个参数的委托类型

委托类型 签名 含义
Action void Action() 不需要参数且无返回值的方法
Func<TResult> TResult Func() 不需要参数,但返回 TRsult 类型对象的方法
Func<Task> Task Func() 不需要参数,但返回简单 Task 对象的方法
Func<Task<TResult>> Task<TResult> Func() 不需要参数,但返回 Task<T> 类型对象的方法

4个 await 语句的示例:

static class MyClass
    {
        public static async Task DoWorkAsync()
        {
            //Run(Action)
            await Task.Run(() => Console.WriteLine(5.ToString()));
            //Run(TResult Func())
            Console.WriteLine((await Task.Run(() => 6)).ToString());
            //Run(Task Func())
            await Task.Run(() => Task.Run(() => Console.WriteLine(7.ToString())));
            //Run(Task<TResult> Func())
            int value = await Task.Run(() => Task.Run(() => 8));

            Console.WriteLine(value.ToString());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Task t = MyClass.DoWorkAsync();
            t.Wait();
            Console.WriteLine("Press Enter key to exit");
            Console.ReadKey();
        }
    }

Lambda 函数() = > GetSum(5,6) 满足 Func<TResult> 委托

int value = awaite Task.Run(() => GetSum(5,6));

三、取消一个异步操作

一些 .NET 异步方法允许你请求终止执行。
System.Threading.Tasks 命名空间中有两个类是为此目的而设计的:CancellationToken 和 CancellationTokenSource。

  • CancellationToken 对象包含一个任务是否应被取消的信息。
  • 拥有 CancellationToken 对象的任务需要定期检查其令牌(token)状态。如果 CancellationToken 对象的 IsCancellationRequested 属性为 true,任务需停止其操作并返回。
  • CancellationToken 是不可逆的,并且只能使用一次。也就是说,一旦 IsCancellationRequested 属性被设置为 true,就不能更改了。
  • CancellationTokenSource 对象创建可分配给不同任务的 CancellationToken 对象。任务持有 CancellationTokenSource 的对象都可以调用其 Cancel 方法,这会将 CancellationTToken 的 IsCancellationRequested 属性设置为 true。

使用这两个类,触发取消行为:

 class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken token = cts.Token;

            MyClass mc = new MyClass();
            Task t = mc.RunAsync(token);

            //Thread.Sleep(3000);
            //cts.Cancel();

            t.Wait();
            Console.WriteLine($"Was Cancelled:{ token.IsCancellationRequested }");

            Console.ReadKey();
        }

        class MyClass
        {
            public async Task RunAsync(CancellationToken ct)
            {
                if (ct.IsCancellationRequested)
                    return;

                await Task.Run(() => CycleMethod(ct), ct);
            }

            void CycleMethod(CancellationToken ct)
            {
                Console.WriteLine("Starting CycleMethod");
                const int max = 5;
                for(int i = 0; i < max;i++)
                {
                    if (ct.IsCancellationRequested)
                        return;
                    Thread.Sleep(1000);
                    Console.WriteLine($"{ i + 1 } of { max } iterations completed");
                }
            }
        }
    }

输出结果:

Starting CycleMethod
1 of 5 iterations completed
2 of 5 iterations completed
3 of 5 iterations completed
4 of 5 iterations completed
5 of 5 iterations completed
Was Cancelled:False

若在 Main 方法里恢复执行这两行代码:

Thread.Sleep(3000);//3秒后取消任务执行
cts.Cancel();

输出结果:

Starting CycleMethod
1 of 5 iterations completed
2 of 5 iterations completed
3 of 5 iterations completed
Was Cancelled:True

四、在调用方法中同步地等待任务

你的代码会继续执行其他任务,但在某个点上可能会需要等待某个特殊 Task 对象完成,然后再继续。

  static class MyDownloadString
    {
        public static void DoRun()
        {
            Task<int> t = CountCharactersAsync("http://illustratedcsharp.com");
            t.Wait();//等待任务 t 结束
            Console.WriteLine($"The task has finished,returning value { t.Result }.");
        }

        private static async Task<int> CountCharactersAsync(string site)
        {
            string result = await new WebClient().DownloadStringTaskAsync(new Uri(site));
            return result.Length;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyDownloadString.DoRun();
            Console.ReadKey();
        }
    }

输出结果:

The task has finished,returning value 5164.

五、Wait 方法用于单一 Task 对象。

1、使用 WaitAll 和 WaitAny

对于一组 Task ,可以等待所有任务都结束,也可以等待某一个任务结束。

  • WaitAll
  • WaitAny
      class MyDownloadString
    {
        Stopwatch sw = new Stopwatch();

        public void DoRun()
        {
            sw.Start();

            Task<int> t1 = CountCharactersAsync(1,"http://illustratedcsharp.com");
            Task<int> t2 = CountCharactersAsync(2, "http://illustratedcsharp.com");

            //Task.WaitAll(t1, t2);
            //Task.WaitAny(t1, t2);

            Console.WriteLine("Task 1: {0} Finished",t1.IsCompleted?"":"Not");
            Console.WriteLine("Task 2: {0} Finished", t2.IsCompleted ? "" : "Not");
            Console.Read();
        }

        private  async Task<int> CountCharactersAsync(int id,string site)
        {
            WebClient wc = new WebClient();
            string result = await wc.DownloadStringTaskAsync(new Uri(site));
            Console.WriteLine("  Call {0} completed:    {1,4:N0} ms", id, sw.Elapsed.TotalMilliseconds);
            return result.Length;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyDownloadString ds = new MyDownloadString();
            ds.DoRun();
            Console.ReadKey();
        }
    }

输出结果:

不使用 WaitAll 和 WaitAny 方法时,t1 和 t2 异步执行

Task 1: Not Finished
Task 2: Not Finished
Call 1 completed: 598 ms
Call 2 completed: 600 ms

1)如果在 DoRun 方法里,恢复 Task.WaitAll(t1, t2); 代码:

Task.WaitAll(t1, t2);
//Task.WaitAny(t1, t2);

输出结果:
使用了 WaitAll 方法后,所有任务都要等待完成,之后代码才能继续执行。

Call 2 completed: 695 ms
Call 1 completed: 617 ms
Task 1: Finished
Task 2: Finished

2)如果在 DoRun 方法里,恢复 Task.WaitAny(t1, t2); 代码:

//Task.WaitAll(t1, t2);
Task.WaitAny(t1, t2);

输出结果:

使用了 WaitAll 方法后,至少有一个 Task 需要等待完成才能继续执行。

Call 2 completed: 659 ms
Call 1 completed: 658 ms
Task 1: Not Finished
Task 2: Finished

2、WaitAll 和 WaitAny 分别还包含4个重载。

签名 描述
WaitAll
void WaitAll(params Task[] tasks) 等待所有任务完成
bool WaitAll(Task[] tasks, int millisecondsTimeout) 等待所有任务完成。如果在超时时限内没有全部完成,则返回 false 并继续执行
void WaitAll(Task[] tasks, CancellationToken token) 等待所有任务完成,或等待 CancellationToken 发出取消信号
bool WaitAll(Task[] tasks, TimeSpan span) 等待所有任务完成。如果在超时时限内没有全部完成,则返回 false 并继续执行
bool WaitAll(Task[] tasks, int millisecondsTimeout, CancellationToken token) 等待所有任务完成,或等待 CancellationToken 发出取消信号。如果在超时时限内没有发生上述情况,则返回 false 并继续执行
WaitAny
void WaitAny(params Task[] tasks) 等待任意一个任务完成
bool WaitAny(Task[] tasks, int millisecondsTimeout) 等待任意一个任务完成。如果在超时时限内没有完成的,则返回 false 并继续执行
void WaitAny(Task[] tasks, CancellationToken token) 等待任意一个任务完成,或等待 CancellationToken 发出取消信号
bool WaitAny(Task[] tasks, TimeSpan span) 等待任意一个任务完成。如果在超时时限内没有完成的,则返回 false
bool WaitAny(Task[] tasks, int millisecondsTimeout, CancellationToken token) 等待任意一个任务完成,或等待 CancellationToken 发出取消信号。如果在超时时限内没有发生上述情况,则返回 false 并继续执行

3、在异步方法中异步地等待任务

可以通过 Task.WhenAll 和 Task.WhenAny 方法,来实现在异步方法中等待一个或所有任务完成。这两个方法称为组合子。

     class MyDownloadString
    {
        Stopwatch sw = new Stopwatch();

        public void DoRun()
        {
            sw.Start();

            Task<int> t = CountCharactersAsync("http://www.baidu.com", "http://illustratedcsharp.com");

            Console.WriteLine("DoRun:   Task {0}Finshed", t.IsCompleted ?"":"Not");
            Console.WriteLine("DoRun:   Result = {0}", t.Result);
        }

        private  async Task<int> CountCharactersAsync(string site1,string site2)
        {
            WebClient wc1 = new WebClient();
            WebClient wc2 = new WebClient();
            Task<string> t1 = wc1.DownloadStringTaskAsync(new Uri(site1));
            Task<string> t2 = wc2.DownloadStringTaskAsync(new Uri(site2));

            List<Task<string>> tasks = new List<Task<string>>();
            tasks.Add(t1);
            tasks.Add(t2);

            await Task.WhenAll(tasks);//关键代码语句

            Console.WriteLine("  CCA:  T1 {0}Finished", t1.IsCompleted ? "" : "Not");
            Console.WriteLine("  CCA:  T2 {0}Finished", t2.IsCompleted ? "" : "Not");

            return t1.IsCompleted ? t1.Result.Length : t2.Result.Length;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyDownloadString ds = new MyDownloadString();
            ds.DoRun();
            Console.ReadKey();
        }
    }

输出结果:
在异步方法内,需要等待所有任务完成后,之后代码才能继续执行。

DoRun: Task NotFinshed
CCA: T1 Finished
CCA: T2 Finished
DoRun: Result = 9269

若把 Task.WhenAll 方法 改为 Task.WhenAny 方法:

输出结果:
在异步方法内,至少有一个任务需要等待完成,之后代码才能继续执行。

DoRun: Task NotFinshed
CCA: T1 Finished
CCA: T2 NotFinished
DoRun: Result = 9269

六、Task.Delay 方法

Thread.Sleep 会阻塞线程,而 Task.Delay 不会阻塞线程,线程还可以继续处理其他工作。

  class Simple
    {
        Stopwatch sw = new Stopwatch();

        public void DoRun()
        {
            Console.WriteLine("Caller:Before call");
            ShowDelayAsync();
            Console.WriteLine("Caller:After call");
            
        }

        private async void ShowDelayAsync()
        {
            sw.Start();
            Console.WriteLine($"  Before Delay:{ sw.ElapsedMilliseconds }");
            await Task.Delay(1000);
            Console.WriteLine($"   After Delay : { sw.ElapsedMilliseconds }");
        }
  
    }

    class Program
    {
        static void Main(string[] args)
        {
            Simple ds = new Simple();
            ds.DoRun();   
            Console.ReadKey();
        }
    }

输出结果:

Caller:Before call
Before Delay:0
Caller:After call
After Delay : 1052

Delay 方法包含4个重载,允许以不同方法来指定时间周期,同时还允许使用 CancellationToken 对象。

签名 描述
Task Delay(int millisecondsDelay) 在以毫秒表示的延迟时间到期后,返回完成的 Task 对象
Task Delay(TimeSpan delay) 在以 .NET TimeSpan 对象表示的延迟时间到期后,返回完成的 Task 对象
Task Delay(int millisecondsDelay, CancellationToken token) 在以毫秒表示的延迟时间到期后,返回完成的 Task 对象。可通过取消令牌来取消该操作
Task Delay(TimeSpan delay, CancellationToken token) 在以 .NET TimeSpan 对象表示的延迟时间到期后,返回完成的 Task 对象。可通过取消令牌来取消该操作

猜你喜欢

转载自blog.csdn.net/chen1083376511/article/details/131267879