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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM