天天看點

【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的調用,其實看圖中的标記即可知道:

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

就短短的幾行代碼便實作了對大量日志的檢索、解析過濾,這得感謝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.ReadLines

File.ReadAllLines

之間的差別從傳回值也可以明顯的分别出來了,一個是流式加載一個立即加載,道理如同上述的第一點(1)

這樣一來也同樣說明了,為什麼架構總是盡量嘗試以一種流式的方式處理資料集,這也是為什麼我們需要傳回IEnumerable的原因了。

了解:Async / Await 異步程式設計淺析

這裡呢,園子很多好文章都已經解釋了怎麼用呀,什麼大緻原理,什麼狀态機什麼的,我這裡就還是引用書中的說辭來通俗的說說,在沒有C#5這麼安逸的異步程式設計之前之後的帶來的感受,舉個小例子看C#團隊幫我們幹了什麼好事兒。

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

不用在意界面,下面把完整代碼貼出來:

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)不給力導緻的,好了該睡覺了.....晚安!老鐵們....

生活本身是很艱難,但是不能成為你不努力的借口!

Remember, Hope is a good thing, maybe the best of things and no good thing ever dies !

繼續閱讀