(19)Task异步:任务创建,返回值,异常捕捉,任务取消,临时变量

    

一、Task任务的创建


    1、用四种方式创建,界面button,info各一。
      


        程序代码

        private void BtnStart_Click(object sender, EventArgs e)
        {
            Task t = new Task(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]new Task.---1");
            });
            t.Start();

            Task.Run(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]Task.Run.---2");
            });

            TaskFactory tf = new TaskFactory();
            tf.StartNew(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]TaskFactory.---3");
            });

            Task.Factory.StartNew(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]Task.Factory.---4");
            });
        }

        private void DisplayMsg(string s)
        {
            if (TxtInfo.InvokeRequired)
            {
                BeginInvoke(new Action(() =>
                {
                    TxtInfo.AppendText($"{s}\r\n");
                }));
            }
            else
            {
                TxtInfo.AppendText($"{s}\r\n");
            }
        }


    
    
    2、Environment.CurrentManagedThreadId与Thread.CurrentThread.ManagedThreadID有什么区别?
        答:简言之:用Environment.CurrentManagedThreadId.
            Environment.CurrentManagedThreadId和Thread.CurrentThread.ManagedThreadID都用于获取当前线程的唯一标识符(Thread ID),但它们有一些区别。
            (1)Environment.CurrentManagedThreadId是一个静态属性,可以从任何地方访问,而Thread.CurrentThread.ManagedThreadID是一个实例属性,只能在当前线程的实例上访问。
            (2)Environment.CurrentManagedThreadId是在.NET Framework 4.0及更高版本中引入的,而Thread.CurrentThread.ManagedThreadID是在较早的版本中引入的。
            (3)Environment.CurrentManagedThreadId返回一个int类型的线程ID,而Thread.CurrentThread.ManagedThreadID返回一个IntPtr类型的线程ID。
            总的来说,两者都可以用于获取当前线程的唯一标识符,但Environment.CurrentManagedThreadId更加方便和易于使用,特别是在多线程编程中。
            
            警告:不要将参数{Environment.CurrentManagedThreadId}放在DisplayMsg中,因为上面四个都是委托,提取的线程是当前线程,委托时执行是在UI线程上,如果放在Display则全是提取的UI线程,这时ID号一般都是1.
    
    
    3、上面四种创建异步操作的区别是什么?
        答:四种用于创建和启动任务(Task)的方法,区别如下:
            (1)new Task(); task.Start();:这是最基本的创建和启动任务的方法。首先,使用new Task()创建一个新的任务对象,然后调用Start()方法来启动任务。这种方式是比较底层和手动的方式,需要手动管理任务的状态和调度。
            (2)task.Run():这是.NET Framework 4.5及更高版本引入的一种简化的方式,用于创建和启动任务。使用Task.Run()方法可以直接创建和启动一个任务,无需手动调用Start()方法。这种方式更加简洁和易于使用,适用于大多数情况。
            (3)Task.Factory.StartNew():这是.NET Framework 4.0及更高版本引入的一种创建和启动任务的方法。Task.Factory.StartNew()方法可以接受一个委托作为参数,并创建并启动一个任务来执行该委托。该方法还提供了一些额外的选项,例如指定任务的调度器、任务的类型等。
            (4)f = new TaskFactory(); f.StartNew():这是使用TaskFactory类来创建和启动任务的方法。首先,创建一个TaskFactory对象,然后调用StartNew()方法来创建并启动任务。TaskFactory类提供了更多的选项和灵活性,例如可以设置任务的调度器、取消任务等。
            总结:Task.Run()是最简洁和常用的创建和启动任务的方法,适用于大多数情况。Task.Factory.StartNew()和TaskFactory.StartNew()提供了更多的选项和灵活性,适用于需要更多控制的情况。new Task(); task.Start();是比较底层和手动的方式,一般不推荐使用,除非有特殊需求。
            
            
            Task.Run 和 TaskFactory.StartNew 都可以用来创建和启动一个新的任务,不同在于:
            (1)静态方法调用方式不同:
            Task.Run 是通过静态方法来创建并启动一个任务。
            TaskFactory.StartNew 是通过实例方法调用 TaskFactory 对象的 StartNew 方法来创建并启动一个任务。
            (2)默认调度器和创建新调度器的方式不同:
            Task.Run 使用当前线程池中的默认调度器来计划任务的执行。
            TaskFactory.StartNew 可以选择使用当前线程池中的默认调度器,或者使用自定义的任务调度器来计划任务的执行。
            (3)返回值类型不同:
            Task.Run 返回一个泛型的 Task<TResult> 对象,该对象表示一个可以异步执行的操作并返回结果。
            TaskFactory.StartNew 返回一个 Task 对象,该对象表示一个可以异步执行的操作,但不返回结果。
            
            
    
    4、Task.Factory介绍
        Task.Factory是.NET Framework中的一个类,用于创建和管理任务(Task)的工厂。它是Task类的一个静态属性,提供了一些方法和属性来创建和管理任务。
        Task.Factory提供了以下几个常用的方法:
        (1)Task.Factory.StartNew(Action action):创建并启动一个任务,该任务执行指定的动作(Action)。
        (2)Task.Factory.StartNew(Action<object> action, object state):创建并启动一个任务,该任务执行指定的动作,并传递一个对象作为参数。
        (3)Task.Factory.StartNew(Func<TResult> function):创建并启动一个任务,该任务执行指定的函数(Func<TResult>),并返回一个结果。
        (4)Task.Factory.StartNew(Func<object, TResult> function, object state):创建并启动一个任务,该任务执行指定的函数,并传递一个对象作为参数,并返回一个结果。

        private static void Main(string[] args)
        {
            Task.Factory.StartNew(Test, 3);
            Task.Factory.StartNew(Test1, 3);
            Console.ReadKey();
        }

        private static void Test(object obj)
        {
            int n = (int)obj;
            Console.WriteLine(2 * n);
        }

        private static int Test1(object obj)
        {
            int n = (int)obj;
            Console.WriteLine("111");
            return 2 * n;
        }      

 
        
        除了以上的方法,Task.Factory还提供了一些其他的方法和属性,例如:
            - Task.Factory.ContinueWhenAll():创建一个任务,该任务在指定的一组任务全部完成后继续执行。
            - Task.Factory.ContinueWhenAny():创建一个任务,该任务在指定的一组任务中的任何一个完成后继续执行。
            - Task.Factory.FromAsync():将一个异步操作(如BeginXXX和EndXXX方法)转换为一个任务。
            - Task.Factory.CancellationToken:获取一个取消标记(CancellationToken),用于取消任务的执行。
        使用Task.Factory可以更方便地创建和管理任务,它提供了一些便捷的方法和属性,使得任务的创建和控制更加灵活和高效。
    


二、异步返回值的认识


    1、异步任务没有返回值时,类型为Task,而不是void
        
        在异步方法中使用Task作为返回类型,即Task或Task<T>,是为了表示异步操作的完成状态和结果。

        异步方法通常会执行一些耗时的操作,例如网络请求、文件读写或计算密集型任务。为了避免阻塞主线程或UI线程,我们可以将这些耗时的操作放在一个异步方法中,并使用await关键字等待操作完成。

        如果异步方法没有返回值,我们可以将返回类型设置为Task。这样,我们可以使用await关键字等待异步操作的完成,并确保异步方法执行完毕后再执行下一步操作。        例如:假设我们有一个异步方法DoSomethingAsync,它执行一些耗时的操作,但不返回任何结果:

        public async Task DoSomethingAsync()
        {
            await Task.Delay(1000); // 模拟一个耗时的操作

            Console.WriteLine("操作完成");
        }


        在调用DoSomethingAsync方法时,我们可以使用await关键字等待异步操作的完成。这样,我们可以确保异步方法执行完毕后再执行下一步操作,而不会阻塞主线程或UI线程。

        public async Task RunAsync()
        {
            Console.WriteLine("开始执行异步操作");

            await DoSomethingAsync();

            Console.WriteLine("异步操作完成");
        }


        上面RunAsync方法调用了DoSomethingAsync方法,并使用await关键字等待异步操作的完成。当异步操作完成后,才会继续执行下一步操作。

        通过将返回类型设置为Task,我们可以清晰地表达异步方法的性质,并使用await关键字等待异步操作的完成。这样,我们可以更好地管理异步操作的执行顺序和结果。
        
        
        人话就是:await有两个功能,一是a wait等会,即等待任务执行完成;二是取得已经完成任务内部的值。
        这个功能都必须是在Task或Task<T>类型的情况下执行,因此,没有返回值的也要标注Task类型。
        
    
    2、为什么返回作两部分看,类型上认定是Task<int>,值上却是int?

        private static async Task<int> Main(string[] args)
        {
            Task<int> task = Task.Run(() =>
            {
                return 32;
            });
            
            Console.ReadKey();
            return await task;
        }  

 
        上面例子,task是Task<int>类型,在await task后应该是int,因此return后仍然是int,但方法前面都使用的是Task<int>。
        
        这是要作两部分看,类型与值分开。
        将返回类型和返回值分开来看可以更清晰地表达方法的异步性质和实际返回的值。

        通过在方法的返回类型中使用Task<int>,我们明确地表示这个方法是一个异步方法,它返回一个Task<int>对象,表示异步操作的结果是一个int类型的值。这可以让调用方明确知道这个方法是一个异步方法,并且需要使用异步的方式来处理它的返回值。

        而在方法内部,通过使用await关键字等待一个Task<int>对象的完成,并获取到它的结果,我们可以方便地使用这个int类型的值,就像普通的同步操作一样。这样,我们可以将异步操作的结果直接作为方法的返回值,而不需要再包装成Task<int>对象。
        
        这种分开对待返回类型和返回值的方式,可以更清晰地表达方法的异步性质,并且方便使用异步操作的结果。同时,也可以帮助其他开发人员更好地理解和使用这个方法。
        如果去掉await,照理返回Task<int>应该和方法前面的类型Task<int>是一致的,但它真的就报错了:


        所以在返回值与返回类型需要理解它为什么这样设计。常用的地方就是方法前面、return,还有一个Result属性。
    
        同样,对于Task<string>与string:
        在某些情况下,可以将Task<string>当作string使用。这是因为Task<string>表示一个异步操作,它的结果是一个string类型的值。

        当我们使用await关键字等待一个Task<string>对象时,它会暂停当前方法的执行,并等待任务完成。一旦任务完成,await表达式就会返回任务的结果,即string类型的值。

        在这种情况下,我们可以直接使用await表达式的结果,就像使用普通的string值一样。例如,我们可以将它赋值给一个string类型的变量,或者将它作为方法的参数传递给接受string类型参数的方法。

        注意,虽然可以将Task<string>当作string使用,但它们是不同的类型。Task<string>表示一个异步操作,而string表示一个同步操作的结果。因此,在一些需要明确区分同步和异步操作的情况下,我们需要注意这两者的区别,并正确地使用它们。
        
        另外,如果方法返回类型是Task,则方法是不需要写return的,写了返回出错,因为理解为void即可。同时它的结果也是无法使用的,也就是没有task.Result这个属性。
        如果方法返回类型是Task<string>,那么必须写return string,这样才匹配。这时可以用结果,用task.Result就是string类型的值。
    


三、异步的异常捕捉


    主要参考:https://blog.csdn.net/weixin_46879188/article/details/118018895,总结得非常好!!!
    
        捕捉异常用try..catch...,针对try捕捉有下面几个特点:
        
    1、同步方法中捕捉异常肯定是可以的。
        try-catch 语句主要用于捕捉异常,而不仅限于同步线程。try 代码块是一个包含可能会引发异常的代码的区域。如果在 try 代码块中的代码引发了异常,系统将跳转到 catch 代码块,并执行相应的异常处理逻辑。
        无论是同步线程还是异步线程,只要代码块中的代码引发了异常,try-catch 都可以捕捉到并进行处理。因此,try-catch 可以用于捕捉同步线程抛出的异常,也可以处理异步线程的异常。
        
        实例认识异步异常:

        private static async Task Main(string[] args)//a
        {
            try
            {
                await TestAsync();//b
            }
            catch (Exception)
            {
                Console.WriteLine("调用方捕捉到异常。");//c
            }
            Console.ReadKey();
            //return 0;//f
        }

        private static async Task TestAsync()//d
        {
            //try
            //{
            //    await Task.Delay(1000);
            //    throw new Exception("被调用方发生异常.");
            //}
            //catch (Exception)
            //{
            //    Console.WriteLine("异步中发生异常。");
            //}
            await Task.Delay(1000);//e
            throw new Exception("被调用方发生异常.");
        }   

 
        (1)a处一般在控制台返回值是void或int,写void会出错。因为前面的async一般不与void配套,它会让异常无法捕捉(特殊情况与void配套是winform中的控件事件,有专门特殊的异常处理这个问题).因此,此处改为Task。当然也可以是Task<int>,此时必须有f处的配合。
            在异步方法当async方法没有明确的返回类型时,例如async void,则无法使用await来等待该方法的完成,也无法直接捕获该方法中可能发生的异常。这样可能会导致调用方无法正确处理或追踪异步方法的执行结果。
            此外,使用Task返回类型还可以方便地与其他异步方法进行组合和串联。通过返回Task,可以轻松建立异步操作的依赖关系,编写更具表达力和可读性的异步代码。
            虽然某些情况下可以省略返回类型,例如async void Main,但在控制台应用程序中,Main方法是应用程序的入口方法,主要用于启动和协调应用程序的执行。使用async Task<int>作为Main方法的返回类型可以更好地处理异步操作和异常,以便在应用程序退出时提供适当的退出代码。中,使用Task返回类型可以提供更好的异步操作等待和异常处理的支持,使得异步代码更具可读性、可维护性和可靠性。
        
        (2)d与e配套说明是一个异步操作。哪怕没有e处,因为d处async的显式说明,也是一个异步操作。
            TestAsync方法是一个异步方法,并且使用async关键字进行标记。即使没有显式使用Task.Run或者类似的调用进行异步执行,await Task.Delay(1000)语句仍然会在异步上下文中执行。因此,异步方法会将该任务(即Task.Delay)放入任务队列中,然后立即返回给调用方,而不会等待任务的完成。
            即使在异步方法中没有任何await语句,该方法仍然被认为是异步的。在异步方法中,使用async关键字声明方法为异步方法,表示该方法可能会执行异步操作。即使方法体内没有await语句,该方法仍然可以被视为异步方法。
            如果没有await语句,该异步方法会立即执行完毕,并返回一个已完成的Task对象。这个Task对象表示异步操作已经完成,但实际上并没有异步操作发生。这种情况下,异步方法的执行方式与普通的同步方法类似,只是在方法签名上使用了async关键字。它不会创建新的线程或者引入异步操作。注意,这种情况下的异步方法可能会有一些行为变得不明确,因为没有异步操作发生。通常情况下,我们应该在异步方法中使用await语句来等待异步操作的完成,以确保异步方法的正常执行和正确的行为。
            
            因此,即使没有e处,因为异步原因,出现的异常不会在本方法内中断,它会通过返回Task"传染"到主程序调用方,所以在主调方用try会捕捉到异常。
            
        (3)d处,异步方法内部发生异步,并没有让异步方法中断。
            异步方法中的异常不会立即中断整个程序的执行,而是通过Task对象将异常传播到调用方,直到被捕获或者传播到顶层方法。使用try-catch块可以有效地捕获和处理异步方法中的异常,以确保程序的正常执行。
            
    
    2、2.Task方法内部try…catch可以捕捉异常
        线程内部出现异常,所以首选处理方式是在Task中使用try…catch…把异常处理掉。
        try...catch只能捕捉到在try块中发生的异常。如果异常发生在try块之外的代码中,将无法被捕捉到。因此,在编写代码时,应该将可能引发异常的代码放在try块中,以便能够及时捕捉并处理异常。
        
        注意,catch块的顺序很重要。如果将父类异常的catch块放在子类异常的catch块之前,父类异常的catch块将永远不会被执行,因为子类异常的catch块已经捕捉到了异常。因此,在编写代码时,应该根据具体的需求和异常的继承关系来确定catch块的顺序。

        try
        {
            // 异步方法调用
            await MyAsyncMethod();
        }
        catch (ChildException ex)
        {
            // 捕捉子类异常
        }
        catch (ParentException ex)
        {
            // 捕捉父类异常
        }


    
    3、Task方法内部异常未处理(异常消失在外太空。),外部如何捕捉?

        private static void Main(string[] args)
        {
            try
            {
                Task task = Task.Run(() => TestAsync());
                task.Wait();//a
            }
            catch (Exception)
            {
                Console.WriteLine("捕捉到异步中的异常。");
            }
            Console.ReadKey();
        }

        private static Task TestAsync()//d
        { throw new Exception("异步中抛出异常"); }


        上面不加入a处语句,在task异步中引发的异常是不会捕捉的,这个异常会消失在"外太空",如果这个"太空"是UI,那么这个UI可能崩溃。
        上面a处,使用Task的Wait方法来等待一个异步任务完成时,如果异步任务抛出了异常,该异常会被封装在一个AggregateException中,并且不会立即抛出。如果没有显式地处理这个AggregateException,它将会被传递给调用栈的顶层,最终导致程序崩溃。
        当没有加入a处的task.Wait()时,异步任务TestAsync()抛出的异常没有被及时处理,因此没有捕捉到异常。而加入a处的task.Wait()方法会阻塞当前线程,等待异步任务完成,同时会将异步任务抛出的异常重新抛出,使得异常能够被try...catch块捕捉到。
        
        注意:
        当一个异步操作抛出异常时,异常会被封装在 Task 对象中。在等待任务完成之前,异常并不会立即传递给调用线程。相反,它会一直保留在 Task 对象中,直到调用方显式等待(通过等待表达式或其他方式)任务完成。
        
        所以,在Task task=处并没有捕捉到异常(尽管task里面有异步信息),但当显示等待时,就抛出异常,从而try捕捉到。
    
    4、线程异常的捕获方法
        线程异常的捕获方法有阻塞型捕获和异步捕获两种。
        阻塞型捕获异常就是:
            当一个或多个线程启动后,我们就一直等待,等到所有线程执行完成,判断这些线程中是否出现异常,而后再执行后续的代码。
        异步型捕获异常就是:
            一个线程启动后,我们不去等待他执行完成,而知直接执行后续其他代码。当线程出现异常时,会自动返回异常信息,或者在需要时主动去获取线程异常信息。        (1)Wait(),Result, GetAwaiter().GetResult() (阻塞型捕获)
            Task中抛出的异常可以捕获,但是也不是直接捕获,而是由调用Wait()方法或者访问Result属性的时候获得异常,优先以AggregateException类型抛出异常,如果没有AggregateException异常捕获的话则以Exception抛出异常。 GetAwaiter().GetResult()方法以Exception抛出异常。
            例子见上面3中a处的wait.
            
            问:GetAwaiter()是什么?
            答:GetAwaiter() 是 Task 类型和其他 awaitable 对象的一个方法,它返回一个可以用于等待异步操作完成的 TaskAwaiter 对象。
                TaskAwaiter 类型是为了支持 await 操作符而设计的。它提供一些特殊的方法和属性,以便在等待异步操作时进行更细粒度的控制和访问。
                通过调用 GetAwaiter() 方法,可以获得对应的 TaskAwaiter 对象,然后可以使用其提供的方法和属性。
                TaskAwaiter 常用的方法和属性:
                    IsCompleted:获取一个布尔值,指示异步操作是否已经完成。
                    GetResult():获取异步操作的结果。如果异步操作尚未完成,此方法会阻塞当前线程直到操作完成。
                    OnCompleted(Action continuation):设置一个继续操作,当异步操作完成时调用指定的委托。这允许在任务完成后执行额外的逻辑。

        private static void Main(string[] args)
        {
            try
            {
                Task task = Task.Run(() => TestAsync());
                task.GetAwaiter().GetResult();//a
            }
            catch (Exception)
            {
                Console.WriteLine("捕捉到异步中的异常。");
            }
            Console.ReadKey();
        }

        private static Task TestAsync()
        { throw new Exception("异步中抛出异常"); }


                a处:异步操作的结果可以通过调用Task对象的GetAwaiter().GetResult()方法来获取。这个方法会阻塞当前线程,直到异步操作完成并返回结果。
                如果异步操作的返回类型是void,那么GetResult()方法将返回一个void值。这意味着它没有结果可供获取。
                如果异步操作的返回类型是Task或Task<T>,那么GetResult()方法将返回异步操作的结果。如果异步操作已经完成,那么GetResult()方法会立即返回结果。如果异步操作还没有完成,那么GetResult()方法会阻塞当前线程,直到异步操作完成并返回结果。
                注意:
                使用GetResult()方法来获取异步操作的结果时,如果异步操作抛出了异常,异常会被封装在一个AggregateException中,并且会在调用GetResult()方法时被重新抛出。
    
    
        (2)使用ContinueWith捕获异常(异步型捕获)(推荐)
            但是如果没有返回结果,或者不想调用Wait()方法,该怎么获取异常呢?就用ContinueWith。

            Task<int> task = Task.Run(async () =>
            {
                await Task.Delay(300);
                Console.WriteLine("异步进行中...");
                throw new Exception("发生异常."); //a
                return 4; //b
            });
            task.ContinueWith(t =>
            {
                Console.WriteLine(t.Exception.Message.ToString());
            }, TaskContinuationOptions.OnlyOnFaulted);//c


            异步在a处抛出异常,在异步线程中b是不会再执行的,就返回了。c处用了continuewith发生错误时会执行里面的代码(相当于捕捉异步)。
            这是一种用于处理异步操作中发生错误的常见方法。通过使用 ContinueWith 方法,您可以在任务完成或出现错误时执行特定的后续操作。
            
            原文作者为了简便写了两个扩展方法,以便直接使用:

            public static Task Catch(this Task task)
            {
                return task.ContinueWith<Task>(delegate (Task t)
                        {
                            if (t != null && t.IsFaulted)
                            {
                                AggregateException exception = t.Exception;
                                Trace.TraceError("Catch exception thrown by Task: {0}", new object[]
                                {
                                     exception
                                });
                            }
                            return t;
                        }).Unwrap();
            }

            public static Task<T> Catch<T>(this Task<T> task)
            {
                return task.ContinueWith<Task<T>>(delegate (Task<T> t)
                        {
                            if (t != null && t.IsFaulted)
                            {
                                AggregateException exception = t.Exception;
                                Trace.TraceError("Catch<T> exception thrown by Task: {0}", new object[]
                                {
                                        exception
                                });
                            }
                            return t;
                        }).Unwrap<T>();
            }     

       
            大神就是不一样,为了进一步学习unwrap又看了一堆参考...
            
            
    5、ContinueWith应该注意的问题
        Task.Run(...)一般会安排立即启动执行。
        
        Task.Run(...).ContinueWith(...)是前面run执行完后,马上继续执行continuewith后面的。至于最后用await或Result,是为了确认整个任务链已经全部完成。因此,整个任务任务链一出现就开始执行。
        
        它与Linq是有区别的。Linq只是一个计划或者说是一个操作链,它定义了在任务完成后要执行的操作,直到真正使用时,才会触发执行任务和操作链,这种延迟执行的特性可以提高代码的灵活性和效率,只有在需要结果时才真正执行任务和操作。

        Task<int> task1 = Task.Run<int>(() =>
        {
            Console.WriteLine("第一个任务完成");
            return 3;
        }).ContinueWith(t =>
        {
            Console.WriteLine("第二个任务完成");
            return t.Result + 2;
        });

        await Task.Delay(500);
        Console.WriteLine("最后任务已经完成.");


        结果;
            第一个任务完成
            第二个任务完成
            最后任务已经完成.
            
        注意:ContinueWith()后面一般跟带一个参数的Action<Task<TResult>>或Func<Task<TResult>>,这个参数就是前面Run运行完成后返回的任务。
        
            
    6、UnWrap()是什么?
            
        (1)Unwrap 方法是 Task 类的一个方法,用于展开嵌套任务。当一个任务返回另一个任务时,我们称之为嵌套任务。通过调用 Unwrap 方法,可以将嵌套任务的结果展开为外层任务的结果。
        具体来说,当一个 Task<Task<T>> 中有嵌套的内部任务时,调用 Unwrap 方法会将内部任务展开,并返回 Task<T> 类型的结果。这样,我们可以更方便地处理嵌套任务的结果,而不需要手动访问内层任务。
        形象的比喻就是:可以将嵌套的Task对象看作是一个盒子,里面又装着另一个盒子。Unwrap()方法的作用就是打开最外层的盒子,将内层的盒子取出来,以便我们可以直接处理内层的盒子。这样,我们可以更方便地对内层的盒子进行操作,而无需关心它是否嵌套在另一个盒子中(即不必外层的Task)。
        Unwrap()方法主要用于处理异步嵌套任务,以简化代码和提高可读性。在同步编程中并不适用。
        (2)使用Unwrap()方法的目的是为了方便地获取内层Task的结果,同时保留外层Task以提供更多的上下文和可读性。这样,我们既可以获取内层Task的结果,又可以在需要时对外层Task进行进一步处理。

        private static void Main(string[] args)
        {
            NestedTaskUnwrap();
            Console.ReadKey();
        }

        private static async void NestedTask()
        {
            //Task返回Task<Task<string>>
            //第一个await后result类型为Task<string>
            Task<string> result = await Task.Run<Task<string>>(() =>
            {
                var task = Task.Run<string>(() =>
                {
                    Task.Delay(1000).Wait();
                    return "Mgen";
                });
                return task;
            });
            Console.WriteLine(await result);//第二个await后才会返回string
        }

        private static async void NestedTaskUnwrap()
        {
            //Task返回Task<Task<string>>
            //await后类型为Task<string>,Unwrap后result类型为string
            string result = await Task.Run<Task<string>>(() =>
            {
                var task = Task.Run<string>(() =>
                {
                    Task.Delay(1000).Wait();
                    return "Mgen";
                });
                return task;
            }).Unwrap();
            Console.WriteLine(result); //不需要await,result已经是string
        }


        
        下面可以看出内外层Task的关系:

        Task<Task<int>> nestedTask = Task.Run<Task<int>>(() =>
                                      {
                                          return Task.Run(() =>
                                          {
                                              return 42;
                                          });
                                      });
        Console.WriteLine(nestedTask.Result);
        Console.WriteLine(nestedTask.Result.Result);//42

        Task<int> innerTask = nestedTask.Unwrap();
        Console.WriteLine(innerTask.Result);//42


        
        (3)Unwrap()方法是为了处理Task<T>和内部类型T之间的特殊关系而设计的。

        在异步编程中,有时候我们可能会在一个任务中返回另一个Task<T>类型的任务。这种情况下,我们通常希望获取内部任务的结果,而不是包装的任务本身。

        Unwrap()方法的主要目的就是为了解决这个问题。它会将嵌套的Task<T>展开,返回内部任务的结果,而不是返回一个嵌套的任务。这样,我们就可以直接获取内部任务的结果,而无需处理嵌套的任务结构。

        注意,Unwrap()方法只能应用于嵌套的Task<Task<T>>结构。如果没有嵌套的任务,或者嵌套的任务不是Task<Task<T>>类型,那么调用Unwrap()方法将没有任何效果。
            
            
    7、异步方法中捕捉异常
        
        (1)async…await可以捕捉异常

        private static async Task TestAsync()
        {
            try
            {
                await Task.Run(() =>
                {
                    throw new Exception("异步中抛出异常。");
                });
            }
            catch (Exception)
            {
                await Console.Out.WriteLineAsync("捕捉到异常");
            }
        }    

    
        
            
        (2)C# 异步方法,尽量避免使用async void而是要用async Task
        try-catch 块只能捕获当前代码块中引发的异常,并不能捕获在 SynchronizationContext 中引发的异常。
        当在 async void 方法中引发异常时,该异常将直接在调用线程的 SynchronizationContext 上引发,而不会传递到调用方代码中。这意味着无法使用 try-catch 块捕获从 async void 方法引发的异常,而且该异常可能会导致程序崩溃。
        所以避免使用async void。
        
        注意:
        在 WinForms 控件中,可以在 async void 方法中捕获异常的原因是因为 WinForms 控件自身实现了一个 SynchronizationContext,称为 WindowsFormsSynchronizationContext。这个特定的 SynchronizationContext 允许在 async void 方法中捕获异常并将其传递到调用方代码中。
        WindowsFormsSynchronizationContext 会捕获从 async void 方法引发的异常,并将其封装在一个特殊的异常对象中,然后将其传递到调用方的异常处理代码中。这样,就可以在 WinForms 控件中使用 try-catch 块来捕获并处理从 async void 方法引发的异常。
        


四、线程取消


    1、8个线程,同时异步执行,当有一个异步停止时,其余将同时停止。
        8个并行时,前方设置一个信号灯,有异常者开灯,其余线程看到灯亮全都自己停止。
        界面:一个button,一个textbox
     


        程序:

        private void BtnRun_Click(object sender, EventArgs e)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken ct = cts.Token;
            for (int i = 0; i < 8; i++)
            {
                string key = $"Key_{i}";
                Task.Run(async () =>
                {
                    await Task.Delay(new Random().Next(100, 300));
                    if (ct.IsCancellationRequested)
                    { throw new Exception($"[{key}]异常,线程取消 "); }
                    Display($"[{key}] 正常开始...\r\n");

                    if (key.Equals("Key_6"))
                    {
                        Display($"[{key}] 异常。\r\n");
                        cts.Cancel();
                        { throw new Exception($"[{key}]异常,线程取消 "); }
                    }

                    if (ct.IsCancellationRequested)
                    { throw new Exception($"[{key}]异常,线程取消 "); }
                    Display($"[{key}] 正常结束.\r\n");
                });
            }
        }

        private void Display(string s)
        { Invoke(new Action(() => { TxtInfo.AppendText(s); })); }


        
    2、上面没有使用ct来作第二参数监视,而是使用IsCancellationRequested.
        分别在异步线程的开始与结束进行监视。
        
        缺点,如果是控制台将不断中断程序,在winform中没有此现象
        
        
        问:结果中在发生异步提示“[Key_6] 异常。”后,后面仍然有两个正常结束,为什么?
        答:当Key_6发生异步取消时,在异步代码执行到ct.ThrowIfCancellationRequested()之前,Key_3和Key_1任务已经开始执行,并且在执行到ct.ThrowIfCancellationRequested()之后,检查到了取消请求,并抛出了OperationCanceledException异常,但是由于被抛出的异常在异步线程中被捕获处理,不会影响到其他任务的执行。
            因此,Key_3和Key_1的任务会继续执行,直到完成并显示正常结束。这是因为ct.ThrowIfCancellationRequested()只是一个检查取消请求的方法,它不会强制立即终止任务。
            而且线程的启动,取消,传递信息等有延迟,就算立即进行取消,这个取消的传递,线程的停止(汽车刹车),其它线程(汽车)的查看信号灯及取消都有些许的延迟,有时需要综合考虑最后的结果。
        
        
        问:为什么线程的启动会有延迟?
        答:C#线程的启动可能会有延迟的原因有多种。
            (1)调度延迟:线程的启动需要操作系统进行线程调度,而操作系统可能有其他任务正在执行,因此可能会有一定的延迟。
            (2)线程池延迟:在使用线程池启动线程时,线程池可能已经达到了最大线程数限制,需要等待其他线程完成后才能启动新线程,导致延迟。
            (3)优先级调度:操作系统可能会根据线程的优先级来进行调度,如果有其他高优先级的线程正在执行,低优先级的线程可能会有延迟。
            (4)线程同步:如果在启动线程前需要进行一些准备工作或者等待某些条件满足,这些操作可能会引起线程启动的延迟。
            注意,延迟是不确定的,具体的延迟时间取决于多种因素,包括操作系统的调度算法、系统负载、线程优先级等等。
        
        
        
    3、优化一下:

        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken ct = cts.Token;
        for (int i = 0; i < 8; i++)
        {
            string key = $"Key_{i}";
            Task.Run(async () =>
            {
                await Task.Delay(new Random().Next(100, 300), cts.Token);
                ct.ThrowIfCancellationRequested();

                Display($"[{key}] 正常开始...\r\n");

                if (key.Equals("Key_6"))
                {
                    Display($"[{key}] 异常。\r\n");
                    cts.Cancel();
                    throw new Exception($"[{key}] 异常。");
                }

                ct.ThrowIfCancellationRequested();
                Display($"[{key}] 正常结束.\r\n");
            });
        }  

         
        (1)await Task.Delay(new Random().Next(100, 300), cts.Token)会等待一个随机的延迟时间,同时能够响应取消请求,以便在适当的时候中止任务的执行。
            这个操作会等待指定的时间,但同时也会响应传入的cancellationToken。如果在延迟期间,cancellationToken被取消(也就是调用了cts.Cancel()),则Task.Delay会抛出一个TaskCanceledException,从而导致异步任务进入取消状态。
            
        (2)ct.ThrowIfCancellationRequested() 的作用是在异步任务的执行过程中检查取消请求。如果在调用 cts.Cancel() 之后,CancellationToken 被取消了,那么 ct.ThrowIfCancellationRequested() 将会抛出 OperationCanceledException。
        
            什么是OperationCanceledException,程序会崩溃吗?
            OperationCanceledException 是一个特殊的异常,在异步编程中通常用于表示取消请求。它不会中断整个线程,也不会导致整个程序崩溃。
            当 OperationCanceledException 被抛出时,它将成为一个普通的异常对象,可以被异常处理机制捕获和处理。异步任务中的 OperationCanceledException 不会自动导致整个程序的崩溃,除非该异常没有得到合适的处理。
            通常情况下,在异步任务中抛出 OperationCanceledException 后,你可以在适当的位置捕获该异常并采取相应的处理措施,比如释放资源、清理操作或进行适当的回滚操作。你可以使用 try-catch 块来捕获并处理该异常,以控制程序的行为。
            如果在异步任务中没有正确处理 OperationCanceledException,它可能会传播到调用异步任务的代码中,直到被捕获或引发未处理的异常处理机制。所以确保在适当的地方对 OperationCanceledException 进行捕获和处理,以适应你的代码逻辑和需求。
            
            
        (3)为什么没有在异步方法的参数列表中添加第二个参数 ct,更精细地监视取消?
            如果你已经在异步方法内部使用了 ct.ThrowIfCancellationRequested() 来检查取消标记并抛出异常,那么在参数列表中再添加第二个参数 ct 是没有必要的。再添加第二个参数 ct 只会造成重复的监视,没有额外的好处,反而会影响代码的可读性和性能。
            
        (4)用抛出异步来中断异步是否是浪费?
            答:是的。可以再优化:

            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken ct = cts.Token;
            for (int i = 0; i < 8; i++)
            {
                string key = $"Key_{i}";
                Task.Run(async () =>
                {
                    await Task.Delay(new Random().Next(100, 300), ct);
                    if (ct.IsCancellationRequested)
                    {
                        Display($"[{key}] 取消请求已触发,任务被取消。\r\n");
                        return;
                    }

                    Display($"[{key}] 正常开始...\r\n");

                    if (key.Equals("Key_6"))
                    {
                        Display($"[{key}] 异常。\r\n");
                        cts.Cancel();
                        return;
                    }

                    if (ct.IsCancellationRequested)
                    {
                        Display($"[{key}] 取消请求已触发,任务被取消。\r\n");
                        return;
                    }
                    Display($"[{key}] 正常结束.\r\n");
                });
            }


            使用 return 语句可以直接中断异步方法的执行,并立即返回。这可以替代抛出异常的方式来实现任务的取消。这样可以避免不必要的资源浪费,并提高代码的效率。
            
            
    4、上面有小bug,当有线程取消,但有线程没有启动时,
        线程仍然会启动,只是会因异常而退出。
        优化:在string key = $"Key_{i}";前加一句if (ct.IsCancellationRequested) break;这样直接跳出循环不必再启动线程。
        或者添加第二参数:

        Task.Run(async () =>
        {
            await Task.Delay(new Random().Next(100, 300), ct);
            if (ct.IsCancellationRequested)
            { throw new Exception($"[{key}]异常,线程取消 "); }
            Display($"[{key}] 正常开始...\r\n");

            if (key.Equals("Key_6"))
            {
                Display($"[{key}] 异常。\r\n");
                cts.Cancel();
                { throw new Exception($"[{key}]异常,线程取消 "); }
            }

            if (ct.IsCancellationRequested)
            { throw new Exception($"[{key}]异常,线程取消 "); }
            Display($"[{key}] 正常结束.\r\n");
        },ct);//a   

         
        在上面a处添加第二参数,用于判断是否有取消状态。
        
        问:第二个参数ct的作用是什么?
        答:最大的作用就是在线程尚未启动时,检查ct可以在任务未启动前取消任务。
            如果任务已经取消了,它可以精细化控制取消,比如,可以直接在任务代码中使用 ct.IsCancellationRequested 或 ThrowIfCancellationRequested() 进行取消请求的判断。
            实际上这个时候加与不加好像效果不大,个人感觉除了前面的作用,没啥用,只是说明我添加了“监视”,任务可能被取消。
 


五、临时变量


    1、下面程序的结果是多少?

        for (int i = 0; i < 20; i++)
        {
            Task.Run(() =>
            {
                Console.Write($"{i},");
            });
        }


    
        答案是:20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,
    
    
    2、为什么全是20?
        上面是一个闭包,Task.Run访问外部变量i。闭包参考:
            https://blog.csdn.net/dzweather/article/details/132741575?spm=1001.2014.3001.5501
        
        for循环不到1毫秒就执行完了,i就变成了20,而每一个Task都带着i变量去申请线程,会有延迟,当它们申请成功时,i就已经变成了20,这时再输出就只能是20。于是我们加个延时看看:

        for (int i = 0; i < 20; i++)
        {
            Thread.Sleep(1);
            Task.Run(() =>
            {
                Console.Write($"{i},");
            });
        }

        
        结果变成:2,3,3,4,4,6,6,8,9,10,10,11,12,13,14,16,16,18,18,19,
        可以看到因为延迟原因,并不完整,但细数一下仍然是20个数,因为有些线程最后执行返回时,i已经变化了,所以不是完整的号.
        
        增加等待时间比如Thread.Sleep(100),则上面的数字更完整,越大越完整。
        
    
    3、再优化一下,不用Thread.Sleep,改用t.Wait()

        for (int i = 0; i < 20; i++)
        {
            Task t = Task.Run(() =>
            {
                Console.Write($"{i},");
            });
            t.Wait();
        }


        比上面Sleep更好,因为你无法确定睡多久,线程才申请、执行,完成!!!
        这下直接就显示了所有的0-19号:0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
        因为它是强制等待每次异步结果才进行,所以结果是按顺序出来的。
    
    
    4、经典做法:用临时变量(推荐)
        上面尽管显示正常,但wait需要等待每个线程执行完后,再进行下一个线程,比较费时。

        for (int i = 0; i < 20; i++)
        {
            int j = i;
            Task t = Task.Run(() =>
            {
                Console.Write($"{j},");
            });
        }


        结果:0,2,1,3,4,6,9,10,11,12,8,14,15,16,7,18,19,5,17,13,
        上面结果完整,且完美体现了线程有延迟,同时是并发执行,时间比较快。
        j是一个局部变量,它在每个循环迭代中都会被重新赋值。由于每个任务都引用了j的当前值,所以它们会打印不同的值。这是因为每个任务都会在Task.Run方法内部创建一个闭包,该闭包会捕获当前循环迭代中的j的值。
        
        对于变量i,它是在循环外部声明的,因此每个任务都共享相同的i变量。由于任务可能在循环结束之前开始执行,所以它们会打印相同的值,即循环结束时的i的最终值。
    
    
 

猜你喜欢

转载自blog.csdn.net/dzweather/article/details/132901837
今日推荐