程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 全面解析C#中的異步編程

全面解析C#中的異步編程

編輯:C#入門知識

全面解析C#中的異步編程


當我們處理一些長線的調用時,經常會導致界面停止響應或者IIS線程占用過多等問題,這個時候我們需要更多的是用異步編程來修正這些問題,但是通常都是說起來容易做起來難,誠然異步編程相對於同步編程來說,它是一種完全不同的編程思想,對於習慣了同步編程的開發者來說,在開發過程中難度更大,可控性不強是它的特點。   在.NET Framework5.0種,微軟為我們系統了新的語言特性,讓我們使用異步編程就像使用同步編程一樣相近和簡單,本文中將會解釋以前版本的Framework中基於回調道德異步編程模型的一些限制以及新型的API如果讓我們簡單的做到同樣的開發任務。   為什麼要異步 一直以來,使用遠程資源的編程都是一個容易造成困惑的問題,不同於“本地資源”,遠程資源的訪問總會有很多意外的情況,網絡環境的不穩定機器服務端的故障,會造成很多程序員完全不可控的問題,所以這也就要求程序員需要更多的去保護遠程資源的調用,管理調用的取消、超市、線程的等待以及處理線程長時間沒響應的情況等。而在.NET中我們通常忽略了這些挑戰,事實上我們會有多種不用的模式來處理異步編程,比如在處理IO密集型操作或者高延遲的操作時候不組測線程,多數情況我們擁有同步和異步兩個方法來做這件事。可是問題在於當前的這些模式非常容易引起混亂和代碼錯誤,或者開發人員會放棄然後使用阻塞的方式去開發。   而在如今的.NET中,提供了非常接近於同步編程的編程體驗,不需要開發人員再去處理只會在異步編程中出現的很多情況,異步調用將會是清晰的且不透明的,而且易於和同步的代碼進行組合使用。   過去糟糕的體驗 最好的理解這種問題的方式是我們最常見的一種情況:用戶界面只擁有一個線程所有的工作都運行在這個線程上,客戶端程序不能對用戶的鼠標時間做出反應,這很可能是因為應用程序正在被一個耗時的操作所阻塞,這可能是因為線程在等待一個網絡ID或者在做一個CPU密集型的計算,此時用戶界面不能獲得運行時間,程序一直處於繁忙的狀態,這是一個非常差的用戶體驗。   很多年來,解決這種問題的方法都是做異步花的調用,不要等待響應,盡快的返回請求,讓其他事件可以同時執行,只是當請求有了最終反饋的時候通知應用程序讓客戶代碼可以執行指定的代碼。   而問題在於:異步代碼完全毀掉了代碼流程,回調代理解釋了之後如何工作,但是怎麼在一個while循環裡等待?一個if語句?一個try塊或者一個using塊?怎麼去解釋“接下來做什麼”?   看下面的一個例子:    
public int SumPageSizes(IList<Uri> uris)
        {
            int total = 0;
            foreach (var uri in uris)
            {
                txtStatus.Text = string.Format("Found {0} bytes...", total);
                var data = new WebClient().DownloadData(uri);
                total += data.Length;
            }
            txtStatus.Text = string.Format("Found {0} bytes total", total);
            return total;
        }

 

  這個方法從一個uri列表裡下載文件,統計他們的大小並且同時更新狀態信息,很明顯這個方法不屬於UI線程因為它需要花費非常長的時間來完成,這樣它會完全的掛起UI,但是我們又希望UI能被持續的更新,怎麼做呢?   我們可以創建一個後台編程,讓它持續的給UI線程發送數據來讓UI來更新自身,這個看起來是很浪費的,因為這個線程把大多時間花在等下和下載上,但是有的時候,這正是我們需要做的。在這個例子中,WebClient提供了一個異步版本的DownloadData方法—DownloadDataAsync,它會立即返回,然後在DownloadDataCompleted後觸發一個事件,這允許用戶寫一個異步版本的方法分割所要做的事,調用立即返回並完成接下來的UI線程上的調用,從而不再阻塞UI線程。下面是第一次嘗試:    
public void SumpageSizesAsync(IList<Uri> uris)
        {
            SumPageSizesAsyncHelper(uris.GetEnumerator(), 0);
        }

        public void SumPageSizesAsyncHelper(IEnumerator<Uri> enumerator, int total)
        {
            if (enumerator.MoveNext())
            {
                txtStatus.Text = string.Format("Found {0} bytes...", total);
                var client = new WebClient();
                client.DownloadDataCompleted += (sender,e)=>{
                    SumPageSizesAsyncHelper(enumerator, total + e.Result.Length);
                };
                client.DownloadDataAsync(enumerator.Current);
            }
            else
            {
                txtStatus.Text = string.Format("Found {0} bytes total", total);
            }
        }
 

 

  然後這依然是糟糕的,我們破壞了一個整潔的foreach循環並且手動獲得了一個enumerator,每一個調用都創建了一個事件回調。代碼用遞歸取代了循環,這種代碼你應該都不敢直視了吧。不要著急,還沒有完 。   原始的代碼返回了一個總數並且顯示它,新的一步版本在統計還沒有完成之前返回給調用者。我們怎麼樣才可以得到一個結果返回給調用者,答案是:調用者必須支持一個回掉,我們可以在統計完成之後調用它。   然而異常怎麼辦?原始的代碼並沒有關注異常,它會一直傳遞給調用者,在異步版本中,我們必須擴展回掉來讓異常來傳播,在異常發生時,我們不得不明確的讓它傳播。   最終,這些需要將會進一步讓代碼混亂:    
public void SumpageSizesAsync(IList<Uri> uris,Action<int,Exception> callback)
        {
            SumPageSizesAsyncHelper(uris.GetEnumerator(), 0, callback);
        }

        public void SumPageSizesAsyncHelper(IEnumerator<Uri> enumerator, int total,Action<int,Exception> callback)
        {
            try
            {
                if (enumerator.MoveNext())
                {
                    txtStatus.Text = string.Format("Found {0} bytes...", total);
                    var client = new WebClient();
                    client.DownloadDataCompleted += (sender, e) =>
                    {
                        SumPageSizesAsyncHelper(enumerator, total + e.Result.Length,callback);
                    };
                    client.DownloadDataAsync(enumerator.Current);
                }
                else
                {
                    txtStatus.Text = string.Format("Found {0} bytes total", total);
                    enumerator.Dispose();
                    callback(total, null);
                }

            }
            catch (Exception ex)
            {
                enumerator.Dispose();
                callback(0, ex);
            }
            
        }

 

    當你再看這些代碼的時候,你還能立馬清楚的說出這是什麼JB玩意嗎?   恐怕不能,我們開始只是想和同步方法那樣只是用一個異步的調用來替換阻塞的調用,讓它包裝在一個foreach循環中,想想一下試圖去組合更多的異步調用或者有更復雜的控制結構,這不是一個SubPageSizesAsync的規模能解決的。   我們的真正問題在於我們不再可以解釋這些方法裡的邏輯,我們的代碼已經完全無章可循。異步代碼中很多的工作讓整件事情看起來難以閱讀並且似乎充滿了BUG。   一個新的方式 如今,我們擁有了一個新的功能來解決上述的問題,異步版本的代碼將會如下文所示:  
public async Task<int> SumPageSizesAsync(IList<Uri> uris)
        {
            int total = 0;
            foreach (var uri in uris)
            {
                txtStatus.Text = string.Format("Found {0} bytes...", total);
                var data = await new WebClient().DownloadDataTaskAsync(uri);
                total += data.Length;
            }
            txtStatus.Text = string.Format("Found {0} bytes total", total);
            return total;
        }
 

 

  除了添加的高亮的部分,上文中的代碼與同步版本的代碼非常相似,代碼的流程也從未改變,我們也沒有看到任何的回調,但是這並不代表實際上沒有回調操作,編譯器會搞定這些工作,不再需要您去關心。   異步的方法是用了Task<int>替代了原來返回的Int類型,Task和Task<T>是在如今的framework提供的,用來代表一個正在運行的工作。   異步的方法沒有額外的方法,依照慣例為了區別同步版本的方法,我們在方法名後添加Async作為新的方法名。上文中的方法也是異步的,這表示方法體會讓編譯器區別對待,允許其中的一部分將會變成回調,並且自動的創建Task<int>作為返回類型。   關於這個方法的解釋:在方法內部,調用另外一個異步方法DownloadDataTaskAsync,它快速的返回一個Task<byte[]>類型的變量,它會在下載數據完成以後被激活,到如前為止,在數據沒有完成之前,我們不想做任何事,所以我們使用await來等待操作的完成。   看起來await關鍵字阻塞了線程直到task完成下載的數據可用,其實不然,相反它標志了任務的回調,並且立即返回,當這個任務完成之後,它會執行回調。   Tasks Task和Task<T>類型已經存在於.NET Framework 4.0中,一個Task代表一個進行時的活動,它可能是一個運行在單獨線程中的一個CPU密集型的工作或者一個IO操作,手動的創建一個不工作在單獨線程的任務也是非常容易的:    
static async void ReadAssignedFile()
        {
            byte[] buffer;
            try
            {
                double length = await ReadFileAsync("SomeFileDoNotExisted.txt", out buffer);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        static Task<double> ReadFileAsync(string filePath,out byte[] buffer) 
        {
            Stream stream = File.Open(filePath, FileMode.Open);
            buffer = new byte[stream.Length];
            var tcs = new TaskCompletionSource<double>();
            stream.BeginRead(buffer, 0, buffer.Length, arr =>
            {
                try
                {
                    var length = stream.EndRead(arr);
                    tcs.SetResult(stream.Length);
                }
                catch (IOException ex)
                {
                    tcs.SetException(ex);
                }
            }, null);
            return tcs.Task;
        }
 

 

      一旦創建了一個TaskCompletionSource對象,你就可以返回與它關聯的Task對象,問相關的工作完成後,客戶代碼才得到最終的結果,這時Task沒有占據自己的線程。   如果實際任務失敗,Task從樣可以攜帶異常並且向上傳播,如果使用await將觸發客戶端代碼的異常:    
static async void ReadAssignedFile()
        {
            byte[] buffer;
            try
            {
                double length = await ReadFileAsync("SomeFileDoNotExisted.txt", out buffer);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        static Task<double> ReadFileAsync(string filePath,out byte[] buffer) 
        {
            Stream stream = File.Open(filePath, FileMode.Open);
            buffer = new byte[stream.Length];
            var tcs = new TaskCompletionSource<double>();
            stream.BeginRead(buffer, 0, buffer.Length, arr =>
            {
                try
                {
                    var length = stream.EndRead(arr);
                    tcs.SetResult(stream.Length);
                }
                catch (IOException ex)
                {
                    tcs.SetException(ex);
                }
            }, null);
            return tcs.Task;
        }
 

 

  image   基於任務的異步編程模型 上文中解釋了異步方法應該是的樣子-Task-based asynchronous Pattern(TAP),上文中異步的體現只需要一個調用方法和異步異步方法,後者返回一個Task或者Task<T>。   下文中將介紹一些TAP中的約定,包括怎麼處理“取消”和“進行中”,我們將進一步講解基於任務的編程模型。   Async和await 理解async方法不運行在自己的線程是非常重要的,事實上,編寫一個async方法但是沒有任何await的話,它就將會是一個不折不扣的同步方法:  
static async Task<int> TenToSevenAsync()
        {
            Thread.Sleep(10000);
            return 7;
        }

 

假如你調用這個方法,將會阻塞線程10秒後返回7,這也許不是你期望的,在VS中也將得到一個警告,因為這可能永遠不是想要的結果。   只有一個async方法運行到一個await語句時,它才立即把控制權返回給調用方,然而只有當等待的任務完成之後,它才會真正的返回結果,這意味著你需要確保async方法中的代碼不會做過多的任務或者阻塞性能的調用。下面的實例才是你所期望的效果  
static async Task<int> TenToSevenAsync()
{
    await Task.Delay(3000);
    return 7;
}

 

Task.Delay實際上是異步版本的Tread,Sleep,它返回一個Task,這個Task將會在指定的時間內完成。   時間處理程序和無返回值的異步方法 異步方法可以從其他異步方法使用await創建,但是異步在哪裡結束?   在客戶端程序中,通常的回答是異步方法由事件發起,用戶點擊一個按鈕,一個異步方法被激活,直到它完成,事件本身並不關系方法何時執行完成。這就是通常所說的“發後既忘”   為了適應這種模式,異步方法通常明確的被設計為“發後既忘”-使用void作為返回值替代Task<TResult>類型,這就讓方法可以直接作為一個事件處理程序。當一個void saync的方法執行時,沒有Task被返回,調用者也無法追蹤調用是否完成。    
private async void someButton_Click(object sender, RoutedEventArgs e)
{
    someButton.IsEnabled = false;
    await SumPageSizesAsync(GetUrls()));
    someButton.IsEnabled = true;
}

 


  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved