C#中的非同步編程
進程與線程
程式在啟動時,系統會在記憶體中建立一個進程。進程是程式運行所需資源的集合,這些資源套件括虛地址空間、檔案控制代碼和其他程式運行所需的東西。在進程的內部,系統建立一個稱為線程的核心對象,代表真正執行的程式。當線程被建立時,系統在Main方法的第一行語句處開始執行線程。關於線程: 預設情況下,一個進程只包含一個線程,從程式開始執行到結束; 線程可以派生其他線程,因此一個進程可能包含多個不同狀態的線程,執行程式的不同部分; 一個進程如果包含多個線程,這些線程共用進程的資源; 系統中處理器執行的規劃單元是線程,不是進程。 同步與非同步 同步是指從語句出現的先後順序執行直到完成。 非同步指語句並不嚴格按照出現的順序執行。有時需要在一個新的線程中運行一部分代碼,有時無需建立新的線程,為了提高單個線程的效率,改變代碼的執行順序。
當某個操作需要花費大量的時間進行處理,若是使用同步編程,那麼程式在等待響應的時間內不能處理其他事物,這樣效率比較低;而使用非同步編程時,在進行等待相應的時間內,程式可以利用等待的時間處理其他事物,當得到響應時,再回到響應處繼續執行,這樣程式的效率會更高。 同步方法與非同步方法呼叫 (async/await)
如果某個方法被調用時,等待所有的執行操作完成後在進行後續操作,該方法為同步的;非同步方法呼叫則在該方法處理完成前就回到被調用處。
同步方法樣本:
class Program{ static void Main(string[] args) { MyDownLoadString my=new MyDownLoadString(); my.DoRun(); Console.ReadKey(); }}class MyDownLoadString{ Stopwatch sw=new Stopwatch(); public void DoRun() { const int largeNumber = 600000; sw.Start(); int t1 = CountCharacters(1, "http://www.baidu.com"); int t2 = CountCharacters(2, "https://www.jd.com"); CountToAlrgeNumnber(1,largeNumber); CountToAlrgeNumnber(2,largeNumber); CountToAlrgeNumnber(3,largeNumber); CountToAlrgeNumnber(4,largeNumber); Console.WriteLine("Chars in http://www.baidu.com :{0}",t1); Console.WriteLine("Chars in https://www.jd.com :{0}", t2); } private int CountCharacters(int id, string uristring) { WebClient wc1=new WebClient(); Console.WriteLine("String call {0} : {1, 4} ms",id,sw.Elapsed.TotalMilliseconds); string result=wc1.DownloadString(new Uri(uristring)); Console.WriteLine("call {0} complete: {1, 4} ms", id, sw.Elapsed.TotalMilliseconds); return result.Length; } private void CountToAlrgeNumnber(int id,int largerNumber) { for (int i = 0; i < largerNumber; i++) { } Console.WriteLine(" End Counting{0}: {1,4} ms",id,sw.Elapsed.TotalMilliseconds); }}
運行結果:
被調用的DoRun()方法的執行順序是按照語句的時序進行的,在擷取網址字元長度的時間內,程式進行等待並未做其他動作,整個程式執行完耗時為:779.6366ms
非同步方法呼叫樣本:
static void Main(string[] args) { MyDownLoadStringAsync myAsync=new MyDownLoadStringAsync(); myAsync.DoRun(); Console.ReadKey(); } class MyDownLoadStringAsync{ Stopwatch sw = new Stopwatch(); public void DoRun() { const int largeNumber = 600000; sw.Start(); Task <int> t1 = CountCharacters(1, "http://www.baidu.com"); Task<int> t2 = CountCharacters(2, "https://www.jd.com"); CountToAlrgeNumnber(1, largeNumber); CountToAlrgeNumnber(2, largeNumber); CountToAlrgeNumnber(3, largeNumber); CountToAlrgeNumnber(4, largeNumber); Console.WriteLine("Chars in http://www.baidu.com :{0}", t1.Result); Console.WriteLine("Chars in https://www.jd.com :{0}", t2.Result); } private async Task<int> CountCharacters(int id, string uristring) { WebClient wc1 = new WebClient(); Console.WriteLine("String call {0} : {1, 4} ms", id, sw.Elapsed.TotalMilliseconds); string result = await wc1.DownloadStringTaskAsync(new Uri(uristring)); Console.WriteLine("call {0} complete: {1, 4} ms", id, sw.Elapsed.TotalMilliseconds); return result.Length; } private void CountToAlrgeNumnber(int id, int largerNumber) { for (int i = 0; i < largerNumber; i++) ; Console.WriteLine(" End Counting{0}: {1,4} ms", id, sw.Elapsed.TotalMilliseconds); }}
運行結果:
從運行結果可以看出,當調用DoRun()方法時,執行順序並不是按照語句的時序進行的,當調用
Task<int> t1 = CountCharacters(1, "http://www.baidu.com");Task<int> t2 = CountCharacters(2, "https://www.jd.com");
方法時,在進入方法內部開始等待後,程式回到調用處DoRun()方法,利用等待的時間執行後續的
CountToAlrgeNumnber(1, largeNumber);CountToAlrgeNumnber(2, largeNumber);CountToAlrgeNumnber(3, largeNumber);CountToAlrgeNumnber(4, largeNumber);
輸出操作,當等待完成後,輸出擷取的網址字串長度,整個方法耗時574.4725ms,而同步方法的耗時是779.6336ms。可見,非同步方法呼叫的效率更高。 async/await 特性
C#中使用async/await特性建立非同步方法呼叫。這個特性結構包括三個部分:
調用方法
public void DoRun(){...Task <int> t1 = CountCharacters(1, "http://www.baidu.com");Task<int> t2 = CountCharacters(2, "https://www.jd.com");...}
調用方法調用非同步方法呼叫,在非同步方法呼叫(可能在相同的線程,可能在新的線程)執行任務的時候繼續執行自身任務。
非同步方法呼叫
private async Task<int> CountCharacters(int id, string uristring){ ...string result = await wc1.DownloadStringTaskAsync(new Uri(uristring));...return result.Length;}
非同步方法呼叫非同步執行方法內的任務,進入到非同步執行的任務處,立即返回到調用方法
await 運算式
await運算式指出需要非同步執行的任務,非同步方法呼叫也是到該處返回到調用方法,非同步任務執行的時候,調用方法繼續執行自身任務。非同步方法呼叫中至少包含一個await運算式。
關於非同步方法呼叫的說明:
方法簽名中包含async關鍵字
async在方法傳回值前,該關鍵字用以說明方法中包含至少一個await運算式,該關鍵字並不能建立任何非同步任務。
非同步方法呼叫具備三種傳回型別: void、Task、Task 調用方法與非同步方法呼叫之間的控制流程
調用方法與非同步方法呼叫之間的控制流程圖:
非同步方法呼叫中可能包含多個await語句,當最後一個await語句所建立的非同步任務執行完畢,Task的屬性和傳回值設定完成,await語句後的代碼在非同步方法呼叫內同步執行完成,非同步方法呼叫執行完成並退出。 await運算式
非同步方法呼叫中的await運算式指定一個非同步任務。這個任務可能是一個Task類型的對象,也可能不是。預設情況下,該任務在當前線程下非同步運行
await task;
這個任務是awaitable類型的對象,awaitable類型是指實現了GetAwaiter()的方法的類型,GetAwaiter()方法返回一個awaiter類型對象,awaiter類型包括成員:
>
- bool IsCompleted{get;}
- void OnCompleted(Action);
- void GetResult(); 或 T GetResult();
在實際使用過程中,並不需要自己構建awaitable類型(就好比使用yield return構建枚舉類型(Enumerable)和列舉程式類型(Enumerator)一樣),Task類是awaitable類型,使用Task類的對象在await運算式中即可建立指定的非同步任務。BCL中包含許多非同步方法呼叫,這些非同步方法呼叫可以返回Task* Task.Run()方法
Task.Run()方法可以建立非同步方法呼叫,與await運算式在當前線程建立非同步任務不同的是,Task.Run()會在另一個線程上執行非同步方法呼叫
Task.Run()方法的參數為一個委託,該委託沒有參數,有傳回值,Task.Run()方法的重載:
>
Task Run(Action action);
Task Run(Action action,CancellationToken token);
Task Run(Func function);
Task Run(Func function,CancellationToken token);
Task Run(Func function);
Task Run(Func function,CancellationToken token);
Task Run(Func 非同步方法呼叫的取消
非同步方法呼叫可以進行終止操作。非同步方法呼叫的取消需要用到Task.Run()方法中的第二個參數類型——CancellationToken
Cancellation是System.Threading.Tasks命名空間中的一個類,非同步方法呼叫的終止還需要該命名空間下的另一個類——CancellationTokenSource
>
CancellationTokenSouce cts=new CancellationTokenSource();
CancellationToken token=cts.Token; CancellationTokenSource對象建立可以分配給不同任務的CancellationToken對象。持有CancellationTokenSource的對象可以調用其Cancel方法,將CancellationToken對象的IsCancellationRequested的值設定為True。 CancellationToken對象包含一個任務是否被取消的資訊。持有CancellationToken對象的任務需要定期檢查其狀態。如果CancellationToken對象的IsCancellationRequested的值被設定為True,任務需停止並退出。 CancellationToken的IsCancellationRequested的值設定是無法復原的,只有一次,一旦該值為True,則不能更改了。
值得注意的是,CancellationTokenSource.Cancel()方法的調用並不會進行終止任務的操作,只是給CancellationToken對象的IsCancellationRequested的值設定為True,真正的終止操作是有持有CancellationToken對象的任務在檢查其IsCancellationRequested的值之後所做的終止並退出。
範例程式碼:
>
class Program{ static void Main(string[] args) { CancellationTokenSource cts=new CancellationTokenSource(); CancellationToken token = cts.Token; MyClass mc=new MyClass(); Task t=mc.RunTask(token); Console.WriteLine("Async Action"); Thread.Sleep(3000); cts.Cancel(); t.Wait(); Console.WriteLine("Was Canceled:{0}",token.IsCancellationRequested); Console.ReadKey(); }}class MyClass{ private Stopwatch sw=new Stopwatch(); public async Task RunTask(CancellationToken ct) { if (ct.IsCancellationRequested) return; await Task.Run(()=>CycleMethod(ct),ct); } void CycleMethod(CancellationToken ct) { sw.Start(); Console.WriteLine("Starting CycleMethod at time {0,4}",sw.Elapsed.TotalMilliseconds); const int max = 5; for (int i = 0; i < max; i++) { if (ct.IsCancellationRequested) return; Thread.Sleep(1000); Console.WriteLine("{0} of {1} iterations completed at time {2,4}",i,max,sw.Elapsed.TotalMilliseconds); } }}
運行結果:
上述代碼中注釋以下兩行:
>
//Thread.Sleep(3000); //cts.Cancel();
運行結果:
當執行CancelationTokenSource.Cancel()後,由持有CancelationToken的RunTask任務檢查IsCancelationRequested的屬性值,此時為True,結束任務並退出。 方法的等待
方法的等待分為在調用方法中的等待和非同步方法呼叫中的等待 調用方法中的同步等待
調用方法內可以調用任意數量的非同步方法呼叫,可以先執行其他任務,然後接收非同步方法呼叫返回的Task對象,也可以在等待某一個非同步方法呼叫執行完畢後,在進行後續任務的處理。可以通過Task的執行個體方法task.Wait()對任務進行等待。task.Wait()用於單一Task對象,對多個Task對象的等待,Task類提供兩個靜態方法,Task.WaitAll(),Task.WaitAny(),這兩個方法的區別在於Task.WaitAll()是在調用方法內等待所有的非同步方法呼叫執行完畢後在執行後續的任務,而Task.WaitAny()則是在調用方法內至少等待某一個非同步任務完成,然後執行後續的任務,不必等待所有的非同步方法呼叫執行完畢。 非同步方法呼叫中的非同步等待
在非同步方法呼叫的內部,可以通過await 運算式來等待非同步方法呼叫中的任務,這時控制權會回到調用方法,但在非同步方法呼叫內部可以等待一個或所有任務的完成。通過Task的靜態方法Task.WhenAny()和Task.WhenAll()來實現。
程式碼範例:
class Program{ static void Main(string[] args) { MyDownloadString mc=new MyDownloadString(); mc.DoRun(); Console.ReadKey(); }}class MyDownloadString{ public void DoRun() { Task<int> t = CountCharactersAsync("http://www.baidu.com", "http://www.hao123.com"); Console.WriteLine("DoRun: Task{0} Finished",t.IsCompleted?"":"NOT"); Console.WriteLine("DoRun: Result={0}",t.Result); } private async Task<int> CountCharactersAsync(string str1,string str2) { WebClient wc1=new WebClient(); WebClient wc2=new WebClient(); Task<string> t1= wc1.DownloadStringTaskAsync(new Uri(str1)); Task<string> t2 = wc2.DownloadStringTaskAsync(new Uri(str2)); List<Task<string>> tasks=new List<Task<string>>(); tasks.Add(t1); tasks.Add(t2); await Task.WhenAny(tasks); Console.WriteLine(" CCA: t1 {0} Completed",t1.IsCompleted?"":"NOT"); Console.WriteLine(" CCA: t2 {0} Completed", t2.IsCompleted ? "" : "NOT"); return t1.IsCompleted ? t1.Result.Length : t2.Result.Length; }}
運行結果:
這裡使用WhenAll()方法,當執行到非同步方法呼叫中的await()後,回到調用方法輸出,完成非同步執行,在非同步方法呼叫中的內部其中一個任務執行完畢後,就完成非同步方法呼叫的調用並回到調用方法。這裡將WhenAny改為WhenAll,結果為:
WhenAll()方法則是在非同步方法呼叫內部等待所有的任務完成才退出回到調用方法。
關於非同步基礎先總結到這裡,還有BeginInvoke()和EndInvoke()以及等待——完成,輪詢,回調等非同步執行模式,有興趣的朋友可以自己找找看
參考C#圖解教程2012(第四版)