【C# in depth 第三版】温故而知新(2)

声明


本文欢迎转载原文地址:http://www.cnblogs.com/DjlNet/p/7522869.html


前言


我们接了上一篇的未完待续,接着我们的划重点之行.....哈哈


理解:LINQ中的延迟执行的流式传输和缓冲传输

通俗的来讲就是先把数据准备好也就是取数据的逻辑已经编写好了(也就是构建好了IEumerable可迭代的数据流),但是只是准备好还没加载到内存中,然后在恰当的位置以一种“just-in-time”的方式提供(也就是在触发MoveNext的才去真的取出数据),这就是称为延迟执行。LINQ框架中总是尽量采用流式传输,在调用MoveNext的时候从迭代器中取出一个元素Current项,然后执行处理类似Where或者Cast,然后返回结果,这样一来就较少的占用了存储空间;在某些情况下又不得不采用缓冲传输,比如反转Reverse或者排序OrderBy啥的,就要求数据全部处于可用的状态也就是加载到内存中来执行批处理。类比一下就是流式传输就好像DataReader来每次处理一条记录一样,然后缓冲传输就貌似DataSet整个读取数据一样。(其中流式传输也称为惰性求值,缓冲传输也称为热情求值,它们都属于延迟执行,与其相反的是立即执行,类似返回一个单一的值Max或者ToList之类什么的,从自然的角度来看也是符合人之常情可以理解的说法),再说说Join中延迟执行右边的数据将会被缓冲处理,而左边的数据依然会进行流式处理,所以这就是为什么尽量join对象的数据量尽量小一些的原因,那么同理在在数据库中上述的道理依然行得通。接着我们举个列子来说明延迟执行的好处,这里我们需要遍历一个Logs日志目录递归下面所有的文件的内容,找出Error对应的行内容,注意这里不会一次性加载一个日志文件所有内容,更也不会加载目下下面的所有文件内容,这里就是依赖了框架提供了流式API的调用,其实看图中的标记即可知道:

就短短的几行代码便实现了对大量日志的检索、解析过滤,这得感谢LINQ的流式处理。关于上述代码的红框部分(1)Directory.GetFiles 以及 Directory.EnumerateFiles 两个API之间的区别看名字就一目了然了吧,这里引用一下官方回答:

The EnumerateFiles and GetFiles methods differ as follows: When you use EnumerateFiles, you can start enumerating the collection of names before the whole collection is returned; when you use GetFiles, you must wait for the whole array of names to be returned before you can access the array. Therefore, when you are working with many files and directories, EnumerateFiles can be more efficient.

(2)File.ReadLinesFile.ReadAllLines 之间的区别从返回值也可以明显的分别出来了,一个是流式加载一个立即加载,道理如同上述的第一点(1)
这样一来也同样说明了,为什么框架总是尽量尝试以一种流式的方式处理数据集,这也是为什么我们需要返回IEnumerable的原因了。


理解:Async / Await 异步编程浅析

这里呢,园子很多好文章都已经解释了怎么用呀,什么大致原理,什么状态机什么的,我这里就还是引用书中的说辞来通俗的说说,在没有C#5这么安逸的异步编程之前之后的带来的感受,举个小例子看C#团队帮我们干了什么好事儿。

不用在意界面,下面把完整代码贴出来:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            _synchronizationContext = SynchronizationContext.Current;
        }

        private static readonly HttpClient _httpClient = new HttpClient();
        private static readonly WebClient _webClient = new WebClient();
        private readonly SynchronizationContext _synchronizationContext;
        private const string _url = "http://www.bing.com";

        /// <summary>
        /// ThreadPool方式构建异步
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button1_Click(object sender, EventArgs e)
        {
            this.button1.Enabled = false;
            ThreadPool.QueueUserWorkItem(x =>
            {
                try
                {
                    var result = _webClient.DownloadString(_url);
                    _synchronizationContext.Post(length =>
                    {
                        int temp = Convert.ToInt32(length);
                        this.label4.Text = temp.ToString();
                    }, result.Length);
                }
                catch (Exception exception)
                {
                    // 这里经过测试可以使用静态方法Show,原理应该也是把消息写进Winform的消息泵中,由WinForm框架自身去循环调度触发
                    MessageBox.Show(exception.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);

                    // 同理下面的代码依然可以
                    //_synchronizationContext.Post(msg =>
                    //{
                    //    var message = msg as string;
                    //    MessageBox.Show(message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    //}, exception.Message);
                }
                finally
                {
                    // 测试除UI线程之外的线程访问UI控件异常
                    //this.button1.Enabled = true;

                    _synchronizationContext.Post(empty =>
                    {
                        this.button1.Enabled = true;
                    }, null);
                }
            });
        }

        /// <summary>
        /// Task构建异步
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button2_Click(object sender, EventArgs e)
        {
            this.button2.Enabled = false;

            Task.Factory.StartNew<string>(x =>
            {
                var result = _webClient.DownloadString(_url);
                return result.Length.ToString();
            }, null)
            .ContinueWith(task =>
            {
                if (task.IsFaulted)
                {
                    // faulted with exception
                    Exception ex = task.Exception;
                    while (ex.InnerException != null)
                        ex = ex.InnerException;
                    MessageBox.Show("Error: " + ex.Message);
                }
                else if (task.IsCanceled)
                {
                    // this should not happen 
                    // as you don't pass a CancellationToken into your task
                    MessageBox.Show("Canclled.");
                }
                else
                {
                    // completed successfully
                    if (!string.IsNullOrWhiteSpace(task.Result))
                    {
                        _synchronizationContext.Post(length =>
                        {
                            int temp = Convert.ToInt32(length);
                            this.label3.Text = temp.ToString();
                        }, task.Result);
                    }
                    //MessageBox.Show("Result: " + task.Result);
                }

                _synchronizationContext.Post(empty =>
                {
                    this.button2.Enabled = true;
                }, null);

                //// 测试除UI线程之外的线程访问UI控件异常
                ////this.button2.Enabled = true;
            }, TaskContinuationOptions.ExecuteSynchronously);
        }

        /// <summary>
        /// Async/Await构建异步
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void button3_Click(object sender, EventArgs e)
        {
            this.button3.Enabled = false;
            try
            {
                string temp = await _httpClient.GetStringAsync(_url);
                this.label6.Text = temp.Length.ToString();
            }
            catch (Exception exception)
            {
                while (exception.InnerException != null)
                {
                    exception = exception.InnerException;
                }
                MessageBox.Show(exception.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            finally
            {
                this.button3.Enabled = true;
            }
        }
    }
}

其中这里还需对上面代码 SynchronizationContext 说明一下:正是因为有了它,我们的异步async/await异步函数的后续操作,能够正确的回到UI线程执行(前提是ConfigureAwait(continueOnCaptureedContext : true) 显示的捕获调用者的上下文这里就是UI线程上下文,其中该方法默认也是参数:true ),其实这个玩意儿已经在.NET 2.0都已经有了,当时是为了提供给 BackgroundWork等组件使用,SynchronizationContext 保证了在适当的线程执行委托的概念,以至于我们调用 SynchronizationContext.Post(异步)或者 SynchronizationContext.Send (同步) 发送消息,与在 Winform中的 Control.BeginInvoke和Control.Invoke有异曲同工之妙呀!!!得注意一点就是在不同的环境模型中,同步上下文代表的含义是不一致的咯!!!这里的我们代码中的 SynchronizationContext 就代表了 UI线程的上下文信息。

接着我们通过上面的代码,看到变化点来看到好处以及C#关于异步编程怎么进化的哈,其中采用了三种不同的方式实现同一种需求,单从代码量上面或者复杂度来说都是递减的(因为这里环境是Winform所以要遵循两个原则:1、不要在UI线程上执行耗时的操作 2、不要除了UI线程之外的其他线程访问UI控件,所以代码略多了点),不过从理解上面都还是比较好理解,毕竟代码上面大家一看就应该是知道底层套路都一样,但是从体验或者感受其中包含了异常处理、线程切换自动回到正确的上下文等还是Async/Await的方式最舒服,虽然到了Task的时候有ContinueWith来衔接任务可以解决回调地狱的问题(毕竟ThreadPool可怜的还没有回调机制)。

await 的主要目的是等待耗时操作是可以避免阻塞,当方法执行到 await 表达式就返回了,当完成等待之后,后续操作依然可以回到原先的UI线程去执行,看到这里有木有一种感觉就是async/await已经帮我们把我们自己的手动实现都做好了而且做得更好做得更多,那是因为C#编译器会对所有await都构建一个后续操作,这里后续操作对于我们来就是就是this.label6.Text = temp.Length.ToString();。关于更加详细的解读,以及内部状态机的构造和状态管理等就是比较复杂了,这里博主也不是很清楚,详情参考官方文档或者博客呗以及书中的详解篇幅也是有的,其实一般情况也不需要关心内部构造,需要关心如何去最佳实践即可。

小总结

到这里第二篇文章也差不多了,这本书的划重点也差不多了(个人来看的话,其实呢可能还有其他忽略的地方,后面CLR温故的时候再补充也是可以的),其实再看了第二遍这本书呐,给我最大感受还是对书中某些模棱两可的知识可能更加稍微掌握了些,还有就是在C#发展的里程碑中,在功能性和体验性上面来说,个人觉得还是 LINQ、Async/Await 带来的东西是给开发者最好的礼物,简直就是其他语言模仿或者学习的标杆(原谅博主活在C#的温柔乡中......),哈哈,当然了好的语言设计那肯定是要分享的嘛,不然其他开发者岂不是很难受!!而且在后面的C#6中对异步编程的await关键字做了进一步提升,具体参考微软文档。好了,重点来了,接下来博主呐,就会开始研究框架框架框架(其实也一直有关注和学习,只是感觉不能出文记录),注意是框架而不是架构哦,毕竟架构本身也是由很多框架组建起来的哦就好像基础组建与微服务的关系一样,主要是看看人家怎么设计框架的,然后才是代码是怎么写的.....。最后再说一句:**掌控自己,就是掌控敌人 --盲僧 **!!!

更新(关于优化Task构建异步 2017年9月19日00:53:55)

        /// <summary>
        /// Task构建异步优化
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button4_Click(object sender, EventArgs e)
        {
            this.button4.Enabled = false;

            var task1 = Task.Factory.StartNew<string>(x =>
            {
                var result = _webClient.DownloadString(_url);
                return result.Length.ToString();
            }, null);

            var task2 = task1.ContinueWith(task =>
            {
                if (task.IsFaulted)
                {
                    // faulted with exception
                    Exception ex = task.Exception;
                    while (ex.InnerException != null)
                        ex = ex.InnerException;
                    MessageBox.Show("Error: " + ex.Message);
                }
                else if (task.IsCanceled)
                {
                    // this should not happen 
                    // as you don't pass a CancellationToken into your task
                    MessageBox.Show("Canclled.");
                }
                else
                {
                    // completed successfully
                    if (!string.IsNullOrWhiteSpace(task.Result))
                    {
                        this.label8.Text = task.Result;
                    }
                }

                this.button4.Enabled = true;

            }, TaskScheduler.FromCurrentSynchronizationContext());
        }

优化说明:删除手动使用同步上下文去控制UI元素,而使用了关键的TaskScheduler.FromCurrentSynchronizationContext()来自动使用当前的同步上下文,方法说明:创建一个与当前 System.Threading.SynchronizationContext 关联的 System.Threading.Tasks.TaskScheduler,其实折腾这玩意儿为了啥,也就是为了也能在.Net4.0的环境也就是客户端电脑还处于这个时期的时候,能够正确是姿势编写异步代码且不那么难受就好了,至于说可以使用一个nuget包Microsoft.Bcl.Async 尚未尝试过,道听途说有点小问题没亲测,不过目前来看应该还可以(瞎猜),主要是客户端的电脑人家是win7安装默认也是net4.0,但是呐他们又不想卡主界面导致未响应,其实也是数据库和网络(异地跨国调用,摊手.jpg)不给力导致的,好了该睡觉了.....晚安!老铁们....

posted @ 2017-09-18 10:33  DJLNET  阅读(1068)  评论(4编辑  收藏  举报