• Task 使用详细[基础操作,异步原则,异步函数,异步模式] 更新中...


    线程是创建并发的底层工具,对于开发者而言,想实现细粒度并发具有一定的局限性,比如将小的并发组合成大的并发,还有性能方面的影响。

    Task可以很好的解决这些问题,Task是一个更高级的抽象概念,代表一个并发操作,但不一定依赖线程完成。

    Task从Framework4.0开始引入,Framework4.5又添加了一些功能,比如Task.Run(),async/await关键字等,

    在.NET Framework4.5之后,基于任务的异步处理已经成为主流模式, (Task-based Asynchronous Pattern,TAP)基于任务的异步模式。

    在使用异步函数之前,先看下Task的基本操作。

    一. Task 基本操作

    1.1 Task 启动方式

    Task.Run(()=>Console.WriteLine("Hello Task"));
    
    Task.Factory.StartNew(()=>Console.WriteLine("Hello Task"));

    Task.Run是Task.Factory.StartNew的快捷方式。

    启动的都是后台线程,并且默认都是线程池的线程

    Task.Run(() =>
    {
        Console.WriteLine(
            $"TaskRun IsBackGround:{CurrentThread.IsBackground}, IsThreadPool:{CurrentThread.IsThreadPoolThread}");
    });
    
    Task.Factory.StartNew(() =>
    {
        Console.WriteLine(
            $"TaskFactoryStartNew IsBackGround:{CurrentThread.IsBackground}, IsThreadPool:{CurrentThread.IsThreadPoolThread}");
    });

    如果Task是长任务,可以添加TaskCreationOptions.LongRunning参数,使任务不运行在线程池上,有利于提升性能。

    Task.Factory.StartNew(() =>
    {
        Console.WriteLine(
            $"TaskFactoryStartNew IsBackGround:{CurrentThread.IsBackground}, IsThreadPool:{CurrentThread.IsThreadPoolThread}");
    }, TaskCreationOptions.LongRunning);

     1.2 Task 返回值/带参数

    Task 有一个泛型子类Task<TResult>,允许返回一个值。

    Task<string> task =Task.Run(()=>SayHello("Jack"));
    
    string SayHello(string name)
    {
        return "Hello " + name;
    }
    
    Console.WriteLine(task.Result);

    通过任务的Result属性获取返回值,这是会堵塞线程,尤其是在桌面客户端程序中,谨慎使用Task.Result,容易导致死锁!

    同时带参数的方式也不是很合理,后面可以被async/await方式直接替代。

    1.3 Task 异常/异常处理

    当任务中的代码抛出一个未处理异常时,调用任务的Wait()或者Result属性时,异常会被重新抛出。

    var task = Task.Run(ThrowError);
    try
    {
        task.Wait();
    }
    catch(AggregateException ex)
    {
        Console.WriteLine(ex.InnerException is NullReferenceException ? "Null Error!" : "Other Error");
    }
    
    
    void ThrowError()
    {
        throw new NullReferenceException();
    }

    对于自治任务(没有wait()和Result或者是延续的任务),使用静态事件TaskScheduler.UnobservedTaskException可以在全局范围订阅未观测的异常。

    以便记录错误日志

    1.4 Task 延续

    延续通常由一个回调方法实现,该方法会在任务完成之后执行,延续方法有两种

    (1)调用任务的GetAwaiter方法,将返回一个awaiter对象。这个对象的OnCompleted方法告知任务当执行完毕或者出错时调用一个委托。

    Task<string> learnTask = Task.Run(Learn);
    var awaiter = learnTask.GetAwaiter();
    awaiter.OnCompleted(() =>
    {
        var result = awaiter.GetResult();
        Console.WriteLine(result);
    });
    
    string Learn()
    {
        Console.WriteLine("Learn Method Executing");
        Thread.Sleep(1000);
        return "Learn End";
    }

    如果learnTask任务出现错误,延续代码awaiter.GetResult()将重新抛出异常,其中GetResult可以直接得到原始的异常,如果使用Result属性,只能解析AggergateException.

    这种延续方法更适用于富客户端程序,延续可以提交到同步上下文,延续回到UI线程中。

    当编写库文件,可以使用ConfigureAwait方法,延续代码会运行在任务运行的线程上,从而避免不必要的切换开销。

    var awaiter =learnTask.ConfigureAwait(false).GetAwaiter(); 

    (2)另一种方法使用ContiuneWith

    Task<string> learnTask = Task.Run(Learn);
    learnTask.ContinueWith(antecedent =>
    {
        var result = learnTask.Result;
        Console.WriteLine(result);
    });
    
    string Learn()
    {
        Console.WriteLine("Learn Method Executing");
        Thread.Sleep(1000);
        return "Learn End";
    }

    当任务出现错误时,必须处理AggregateException, ContiuneWith更适合并行编程场景。

    1.5 TaskCompletionSource类使用

    从如下源码中可以看出当实例化TaskCompletionSource时,构造函数会新建一个Task任务。

    public class TaskCompletionSource
    {
      private readonly Task _task;
      
      /// <summary>Creates a <see cref="TaskCompletionSource"/>.</summary>
      public TaskCompletionSource() => _task = new Task();
      
      /// <summary>
            /// Gets the <see cref="Tasks.Task"/> created
            /// by this <see cref="TaskCompletionSource"/>.
            /// </summary>
            /// <remarks>
            /// This property enables a consumer access to the <see cref="Task"/> that is controlled by this instance.
            /// The <see cref="SetResult"/>, <see cref="SetException(Exception)"/>, <see cref="SetException(IEnumerable{Exception})"/>,
            /// and <see cref="SetCanceled"/> methods (and their "Try" variants) on this instance all result in the relevant state
            /// transitions on this underlying Task.
            /// </remarks>
      public Task Task => _task;
    }

    它的真正的作用是创建一个不绑定线程的任务。

    eg: 可以使用Timer类,CLR在定时之后触发一个事件,而无需使用线程。

    实现通用Delay方法:

    Delay(5000).GetAwaiter().OnCompleted(()=>{ Console.WriteLine("Delay End"); });
    
    Task Delay(int millisecond)
    {
        var tcs = new TaskCompletionSource<object>();
        var timer = new System.Timers.Timer(millisecond) { AutoReset = false };
        timer.Elapsed += delegate
        {
            timer.Dispose();
            tcs.SetResult(null);
        };
        timer.Start();
        return tcs.Task;
    }

    这个方法类似Task.Delay()方法。

    二. 异步原则(补充)

    同步操作:先完成其工作再返回调用者

    异步操作:大部分工作则是在返回调用者之后才完成的,也称非阻塞方法。

    异步编程的原则:

    (1)以异步的方式编写运行时间很长(或者可能很长)的函数,会在一个新的线程或者任务上调用这些函数,从而实现需要的并发性。

    (2)异步方法的并发性是在长时间运行的方法内启动的,而不是从这个方法外启动的。

    • I/O密集的并发性的实现不需要绑定线程(如1.5节的例子所示),因此可以提高可伸缩性和效率。
    • 富客户端应用程序可以减少工作线程的代码,因此可以简化线程安全性的实现。

    Task支持延续,因此非常适合进行异步编程的,如1.5节的Delay方法。

    在计算密集的方法中,我们使用Task.Run创建线程相关的异步性。但是异步编程的不同点在于,更希望将异步放在底层调用图上,

    因此富客户端应用程序的高层方法就可以一直在UI线程上运行,访问控件、共享状态而不用担心会出现线程安全问题。

    看Task.Run的例子:

    //粗粒度并发
    Task.Run(() => DisplayPrimeCounts());
    
    /// <summary>
    /// 显示素数个数
    /// </summary>
    void DisplayPrimeCounts()
    {
        for (int i = 0; i < 10; i++)
            Console.WriteLine(GetPrimesCount(i * 1000000 + 2, 1000000) +
                              " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
    
        Console.WriteLine("Done!");
    }
    
    /// <summary>
    /// 获取素数个数
    /// </summary>
    int GetPrimesCount(int start, int count)
    {
        return
            ParallelEnumerable.Range(start, count).Count(n =>
                Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0));
    }

    这是一种粗粒度并发,如果想实现细粒度并发,需要编写异步的方法。

    看异步版本:

    DisplayPrimeCountsAsync();
    
    Task DisplayPrimeCountsAsync()
    {
        var machine = new PrimesStateMachine();
        machine.DisplayPrimeCountsFrom(0);
        return machine.Task;
    }
    
    class PrimesStateMachine        
    {
        TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
        public Task Task { get { return _tcs.Task; } }
    
        /// <summary>
        /// 异步显示素数个数
        /// </summary>
        /// <param name="i"></param>
        public void DisplayPrimeCountsFrom(int i)
        {
            var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
            awaiter.OnCompleted(() =>
            {
                Console.WriteLine(awaiter.GetResult()+" primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
                if (i++ < 10) DisplayPrimeCountsFrom(i);
                else { Console.WriteLine("Done"); _tcs.SetResult(null); }
            });
        }
    
        /// <summary>
        /// 异步获取素数个数
        /// </summary>
        /// <param name="start"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        Task<int> GetPrimesCountAsync(int start, int count)
        {
            return Task.Run(() =>
                ParallelEnumerable.Range(start, count).Count(n =>
                    Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
        }
    }

    可以看到改造异步后的实现方式,很复杂。 GetPrimesCountAsync改为方法内部启动异步,DisplayPrimeCountsFrom通过TaskCompletionSource实现异步。

    这时async和await登场!

    async和await关键字极大的简化了程序的复杂度。

    async/await版本:

    DisplayPrimeCountsAsync();
    
    /// <summary>
    /// 异步显示素数个数
    /// </summary>
    async Task DisplayPrimeCountsAsync()
    {
        for (int i = 0; i < 10; i++)
            Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000) +
                              " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
    
        Console.WriteLine("Done!");
    }
    
    /// <summary>
    /// 异步获取素数个数
    /// </summary>
    Task<int> GetPrimesCountAsync(int start, int count)
    {
        return Task.Run(() =>
            ParallelEnumerable.Range(start, count).Count(n =>
                Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    }

    从编程形式上,有点类似同步方法一样直观简洁。其实async/await编译器也是将其转换为一个状态机。通常我们称之为C#语法糖。

     编译器背后的原理可以参考这篇文章:https://www.cnblogs.com/zh7791/p/9951478.html

    三. 异步函数

    这章开始进入异步函数的使用,由上面一章已经引出async/await关键字。可以使用同步的代码风格编写异步代码,极大地降低了异步编程的复杂度。

    简单捋下async/await

    如下语句中使用了await附加了延续,statement(s)是expression的延续。

    这个“等待”被编译器转化为如下同等功能的代码。

     

     这是如果想要成功编译就必须添加async修饰符,如下图提示。

    async修饰符会指示编译器将await作为一个关键字而非标识符,来避免二义性(C#5之前有可能作为标识符使用),添加async修饰符的方法称为异步函数。

    3.1 富客户端异步函数Demo

    通过WPF的例子展示异步函数在富客户端应用程序中的作用:在执行计算密集的方法时,仍然保持UI的响应,不堵塞UI线程。

     先看同步调用的情况:

    private void ExecuteTaskOnClick(object sender, RoutedEventArgs e)
    {
        TextBoxMessage.Text = "Call Worker" + Environment.NewLine;
        DoSomething();//同步调用
    }
    
    private void DoSomething()
    {
        Thread.Sleep(3000);//模拟计算密集耗时
        TextBoxMessage.Text += "Calculate Done" + Environment.NewLine;
    }

    上图可以清楚的看到,当使用同步调用耗时方法时,UI线程无法响应用户事件请求,TextBox的信息显示也是等耗时方法结束后才更新。

    原因是在耗时方法执行期间,UI线程已经被阻塞,UI线程接收的处理请求都会进入请求队列,无法及时响应(包括鼠标键盘的事件请求,控件更新),很影响用户体验。

    下面看异步版本:

    btnExecuteTaskAsync.Click += (sender, args) => ExecuteTaskAsync();
    
    private async void ExecuteTaskAsync()
    {
        btnExecuteTaskAsync.IsEnabled = false;
        TextBoxMessage.Text = "Call Worker Async" + Environment.NewLine;
        await DoSomethingAsync();//异步调用
        TextBoxMessage.Text += "Calculate Async Done" + Environment.NewLine;
        btnExecuteTaskAsync.IsEnabled = true;
    }
    
    private async Task DoSomethingAsync()
    {
        await Task.Run(() =>
        {
            Thread.Sleep(3000); //模拟计算密集耗时
        });
    }

    更改为异步版本后,在执行耗时任务时,UI线程没有被堵塞,可以正常响应用户事件和控件更新,提高了用户体验。

    3.2 异步调用执行过程

    根据3.1节的例子,整个调用过程如下:

    当用户点击按钮时触发事件,事件调用ExecuteTaskAsync 方法,ExecuteTaskAsync 方法调用DoSomethingAsync方法,而后调用await,而await会使执行点返回给调用者,

    当DoSomethingAsync方法完成(或者出现错误)时,执行点会从停止之处恢复执行DoSomethingAsync后面的代码。

    ExecuteTaskAsync 方法则会'租用'UI线程的时间,即ExecuteTaskAsync 方法在消息循环1中是以伪并发的方式执行的(执行会在UI线程的其他事件处理中穿插进行)。

    在整个伪并发的过程中,只有await的过程中才会进行抢占,这就简化了线程的安全性。DoSomethingAsync会运行在工作线程上,正真的并发发生在DoSomethingAsync方法的Task.Run部分,在Task.Run部分尽量避免访问共享状态和UI组件。

    注1:UI线程上的消息循环的伪代码如下:

    待续。。。

  • 相关阅读:
    【转】Java并发编程:阻塞队列
    【计算机二级Java语言】卷005
    【计算机二级Java语言】卷004
    【计算机二级Java语言】卷003
    【计算机二级Java语言】卷002
    【计算机二级Java语言】卷001
    【计算机二级C语言】卷020
    【计算机二级C语言】卷019
    【计算机二级C语言】卷017
    【计算机二级C语言】卷016
  • 原文地址:https://www.cnblogs.com/YourDirection/p/15765209.html
Copyright © 2020-2023  润新知