IO操作的MDA(Direct memory access)模式:直接訪問記憶體,是一種不經過CPU而直接進行記憶體數據存儲的數據交換模式,幾乎可以不損耗CPU的資源; CLR所提供的非同步編程模型就是充分利用硬體的DMA功能來釋放CPU的壓力;使用線程池進行管理,非同步將工作移交給線程池中的某個工作線程來 ...
IO操作的MDA(Direct memory access)模式:直接訪問記憶體,是一種不經過CPU而直接進行記憶體數據存儲的數據交換模式,幾乎可以不損耗CPU的資源;
CLR所提供的非同步編程模型就是充分利用硬體的DMA功能來釋放CPU的壓力;使用線程池進行管理,非同步將工作移交給線程池中的某個工作線程來完成,直到非同步完成,非同步才會通過回調的方式通知線程池,讓CLR響應非同步完畢;
它是併發的一種形式,它採用 future 模式或回調(callback)機制,以避免產生不必要的線程。一個 future(或 promise)類型代表一些即將完成的操作。在 .NET 中,新版 future 類型有Task 和Task<TResult>。
非同步編程模式------利用委托和線程池實現的模式
APM 非同步編程模型,Asynchronous Programming Model C#1.0
EAP 基於事件的非同步編程模式,Event-based Asynchronous Pattern C#2.0
TAP 基於任務的非同步編程模式,Task-based Asynchronous Pattern C#4.0
Async\await簡化非同步編程;任務並行庫,Task Parallel Library C#5
APM
使用IAsyncResult設計模式的非同步操作是通過名為 BeginXXX 和 EndXXX 的兩個方法來實現,這兩個方法分別指開始和結束非同步操作。該模式允許用更少的CPU資源(線程)去做更多的操作,.NET Framework很多類也實現了該模式,同時我們也可以自定義類來實現該模式(也就是在自定義的類中實現返回類型為IAsyncResult介面的BeginXXX方法和接受IAsyncResult相容類型作為唯一參數的EndXXX方法),另外委托類型也定義了BeginInvoke和EndInvoke方法。例如,FileStream類提供BeginRead和EndRead方法來從文件非同步讀取位元組。這兩個方法實現了 Read 方法的非同步版本。
調用 BeginXXX 後,應用程式可以繼續在調用線程上執行指令,同時非同步操作在另一個線程上執行(如果有返回值還應調用 EndXXX結束非同步操作,並向該方法傳遞BeginXXX 方法返回的IAsyncResult對象,獲取操作的返回值)。
CompletedSynchronously屬性值側重與提示信息,而非操作
訪問非同步操作的結果,APM提供了四種方式:
1.在調用BeginXXX方法的線程上調用EndXXX方法來得到非同步操作的結果;但是這種方式會阻塞調用線程,在知道操作完成之後調用線程才能繼續運行。
2.迴圈查詢IAsyncResult的IsComplete屬性,操作完成後再調用EndXXX方法來獲得操作返回的結果。
3.IAsyncResult的AsyncWaitHandle屬性實現更加靈活的等待邏輯,調用該屬性WaitOne()方法來使一個線程阻塞並等待操作完成;再調用EndXXX方法來獲得操作的結果。WaitHandle.WaitOne()可以指定最長的等待時間,如超時返回false;
4. 在調用BeginXXX方法時提供AsyncCallback委托的實例作為參數,在非同步操作完成後委托會自動調用(AsyncCallback對象)指定的方法。(首選方式)AsyncCallback委托僅能夠調用符合特定模式的方法(只有一個參數IAsyncResult,且沒有返回值);
using System; using System.Collections.Generic; using System.Text; using System.Threading; using System.Runtime.Remoting.Messaging; namespace AsyncCallbackDelegate { public delegate int BinaryOp(int x, int y); class Program { private static bool isDone = false; static void Main(string[] args) { Console.WriteLine("***** AsyncCallbackDelegate Example *****"); Console.WriteLine("Main() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId); BinaryOp b = new BinaryOp(Add); IAsyncResult iftAR = b.BeginInvoke(10, 10, new AsyncCallback(AddComplete), "Main() thanks you for adding these numbers.");//傳入數據 // Assume other work is performed here... while (!isDone) { Thread.Sleep(1000); Console.WriteLine("Working...."); } Console.ReadLine(); } #region Target for AsyncCallback delegate // Don't forget to add a 'using' directive for // System.Runtime.Remoting.Messaging! static void AddComplete(IAsyncResult itfAR) { Console.WriteLine("AddComplete() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("Your addition is complete"); // Now get the result. //AsyncCallback委托的目標無法調用其他方法中創建的委托 //IAsyncResult itfAR 實際上是System.Runtime.Remoting.Messaging命名空間AsyncResult類的一個實例 AsyncResult ar = (AsyncResult)itfAR; //AsyncDelegate靜態屬性返回原始非同步委托引用 BinaryOp b = (BinaryOp)ar.AsyncDelegate; Console.WriteLine("10 + 10 is {0}.", b.EndInvoke(itfAR)); // Retrieve the informational object and cast it to string. //AsyncState屬性獲取 BeginInvoke第四個參數傳入的值 string msg = (string)itfAR.AsyncState; Console.WriteLine(msg); isDone = true; } #endregion #region Target for BinaryOp delegate static int Add(int x, int y) { Console.WriteLine("Add() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId); Thread.Sleep(5000); return x + y; } #endregion } }AsyncCallback
異常捕獲
在同步執行的方法裡面通常處理異常的方式是將可能拋出異常的代碼放到try...catch...finally裡面,之所以能夠捕獲到,是因為發生異常的代碼與調用的代碼位於同一個線程。當調用一個非同步方法發生異常時,CLR會捕獲並且在EndXXX方法時再次將異常拋出拋出,所以非同步調用中的異常在EndXXX方法出捕獲就行了。
class ApmExceptionHandling { public static void Go() { WebRequest webRequest = WebRequest.Create("http://0.0.0.0/"); webRequest.BeginGetResponse(ProcessWebResponse, webRequest); Console.ReadLine(); } private static void ProcessWebResponse(IAsyncResult result) { WebRequest webRequest = (WebRequest)result.AsyncState; WebResponse webResponse = null; try { webResponse = webRequest.EndGetResponse(result); Console.WriteLine("Content length: " + webResponse.ContentLength); } catch (WebException we) { Console.WriteLine(we.GetType() + ": " + we.Message); } finally { if (webResponse != null) webResponse.Close(); } } }
APM WinForm UI線程回調
由於AsyncCallback委托回調是從ThreadPool中的線程執行的,因此對於Winform,如果回調需要操作UI控制項,就需要返回到UI線程去,常用的兩個方法:
1. Control類實現了ISynchronizeInvoke介面,提供了Invoke和BeginInvoke方法來支持其它線程更新GUI界面控制項的機制(將回調方法投遞到創建該控制項的線程中執行)。
Control類的 Invoke,BeginInvoke 內部實現如下:
a) Invoke(同步調用)先判斷控制項創建線程與當前線程是否相同,相同則直接調用委托方法;否則使用Win32API的PostMessage非同步執行,但是Invoke內部會調用IAsyncResult.AsyncWaitHandle等待執行完成。
b) BeginInvoke(非同步調用)使用Win32API的PostMessage 非同步執行,並且返回 IAsyncResult 對象。
使用方式:回調方法中對控制項檢測InvokeRequired值,if true,在該回調中封送一次委托,調用控制項的Invoke/ BeginInvoke方法;
2.GUI(WinForm/WPF)應用程式引入了一個線程處理模型:創建視窗的線程是唯一能對那個視窗進行更新的線程;在GUI線程中,經常需要生成非同步操作,使GUI線程不阻塞並停止響應用戶輸入。然而,非同步操作完成時,由於是用一個線程池線程完成的,而線程池線程不能更新UI控制項。為解決這些問題,FCL定義一個System.Threading.SynchronizationContext(線程同步上下文)的基類,其派生對象負責將一個應用程式模型連接到它的線程處理模型。
GUI線程都有一個和它關聯的SynchronizationContext派生對象,使用其靜態Current屬性獲取:SynchronizationContext sc = SynchronizationContext.Current; 將此對象傳給其他線程,當一個線程池線程需要讓GUI線程更新UI時,調用該對象的sc.Post方法,向Post傳遞一個匹配SendOrPostCallback委托簽名的回調方法(一般是更新UI的操作方法,由GUI線程去執行),以及一個要傳給回調方法的實參。
SynchronizationContext 的Post方法和Send方法的區別:(分別對應於非同步/同步調用)
Post方法將回調方法送人GUI線程的隊列,允許程式池線程立即返回,不進行阻塞;Post方法內部調用了BeginInvoke方法;
Send方法也將回調方法送人GUI線程的隊列,但隨後就會阻塞線程池線程,直到GUI線程完成對回調方法的調用。阻塞線程池線程極有可能造成線程池創建一個新的線程,避免調用該方法;Send方法內部調用了Invoke方法;
對winform來說是 System.Windows.Forms.WindowsFormsSynchronizationContext是其子類.
Winform視窗出現後,UI線程 SynchronizationContext.Current會被綁定賦值,只有UI線程的Current不為null。
Public class SendOrPostUI { public static void Go() { System.Windows.Forms.Application.Run(new MyWindowsForm()); } private static AsyncCallback SyncContextCallback(AsyncCallback callback) { // Capture the calling thread's SynchronizationContext-derived object SynchronizationContext sc = SynchronizationContext.Current; // If there is no SC, just return what was passed in if (sc == null) return callback; // Return a delegate that, when invoked, posts to the captured SC a method that // calls the original AsyncCallback passing it the IAsyncResult argument return asyncResult => sc.Post(result => callback((IAsyncResult)result), asyncResult); } private sealed class MyWindowsForm : System.Windows.Forms.Form { public MyWindowsForm() { Text = "Click in the window to start a Web request"; Width = 400; Height = 100; } protected override void OnMouseClick(System.Windows.Forms.MouseEventArgs e) { // The GUI thread initiates the asynchronous Web request Text = "Web request initiated"; var webRequest = WebRequest.Create("http://Wintellect.com/"); webRequest.BeginGetResponse(SyncContextCallback(ProcessWebResponse), webRequest); base.OnMouseClick(e); } private void ProcessWebResponse(IAsyncResult result) { // If we get here, this must be the GUI thread, it's OK to update the UI var webRequest = (WebRequest)result.AsyncState; using (var webResponse = webRequest.EndGetResponse(result)) { Text = "Content length: " + webResponse.ContentLength; } } } }
比較兩種方法其實差不太多,一個是回調內再次包裝,一個是包裝原來的回調。但是SynchronizationContext業務層與UI分離來講的話是比較好;
EAP
EAP是為了更便於處理UI的更新推出的模式,主要優點:它同Visual Studio UI設計器進行了很好的集成,可將大多數實現了EAP的類拖放到設計平面(design surface)上,雙擊控制項對應的XXXCompleted事件名,會自動生成事件的回調方法,並將方法同事件自身聯繫起來。EAP保證事件在應用程式的GUI線程上引發,允許事件回調方法中的代碼更新UI控制項;
EAP另一重要功能:支持EAP的類自動將應用程式模型映射到它的線程處理模型;EAP類在內部使用SynchronizationContext類。有的EAP類提供了取消、進度報告功能。
FCL中只有17個類型實現了EAP模式,一般有一個 XXXAsync方法和一個對應的XXXCompleted事件,以及這些方法的同步版本:
System.Object的派生類型:
System.Activies.WorkflowInvoke
System.Deployment.Application.ApplicationDeployment
System.Deployment.Application.InPlaceHosingManager
System.Net.Mail.SmtpClient
System.Net.PeerToPeer.PeerNameResolver
System.Net.PeerToPeer.Collaboration.ContactManager
System.Net.PeerToPeer.Collaboration.Peer
System.Net.PeerToPeer.Collaboration.PeerContact
System.Net.PeerToPeer.Collaboration.PeerNearMe
System.ServiceModel.Activities.WorkflowControlClient
System.ServiceModel.Discovery.AnnoucementClient
System.ServiceModel.Discovery.DiscoveryClient
System.ComponentModel.Component的派生類型:
System.ComponentModel.BackgroundWorker
System.Net.NetworkInformation.Ping
System.Windows.Forms.PictureBox(繼承於Control類,Control類派生於Component類)
private sealed class MyForm : System.Windows.Forms.Form { protected override void OnClick(EventArgs e) { // The System.Net.WebClient class supports the Event-based Asynchronous Pattern WebClient wc = new WebClient(); // When a string completes downloading, the WebClient object raises the // DownloadStringCompleted event which will invoke our ProcessString method wc.DownloadStringCompleted += ProcessString; // Start the asynchronous operation (this is like calling a BeginXxx method) wc.DownloadStringAsync(new Uri("http://Wintellect.com")); base.OnClick(e); } // This method is guaranteed to be called via the GUI thread private void ProcessString(Object sender, DownloadStringCompletedEventArgs e) { // If an error occurred, display it; else display the downloaded string System.Windows.Forms.MessageBox.Show((e.Error != null) ? e.Error.Message : e.Result); } }
BackgroundWorker:只有該類型用於可用於執行非同步的計算限制的工作;提供三個事件:
DoWork:向這個事件登記的方法應該包含計算限制的代碼。這個事件由一個線程池線程調用RunWorkerAsync(兩個重載方法,帶參的方法是向DoWork登記的方法的DoWorkEventArgs參數對象的Argument屬性傳值,只能在登記的方法中(如e.Argument)獲取,Result屬性必須設置成計算限制的操作希望返回的值)時引發;
ProgressChanged:向這個事件登記的方法應該包含使用進度信息來更新UI的代碼。這個事件總是在GUI線程上引發。DoWork登記的方法必須定期調用BackgroundWorker的ReportProgress方法來引發ProgressChanged事件;
RunWorkerCompleted:向這個事件登記的方法應該包含使用計算限制操作的結果對UI進行更新的代碼。這個事件總是在GUI線程上引發。Result獲取表示非同步操作的結果;
公共屬性:CancellationPending(標識是否已請求取消後臺操作)、IsBusy(標識是否正在運行非同步操作)、WorkReportsProgress(獲取/設置是否報告進度更新)、WorkerSupportsCancellation(獲取/設置是否支持非同步取消)
公共方法:CancelAsync(請求取消掛起的後臺操作)、ReportProgress、RunWorkerAsync
異常
異常不會拋出。在XXXCompleted事件處理方法中,必須查詢AsyncCompletedEventArgs的Exception屬性,看它是不是null。如果不是null,就必須使用if語句判斷Exception派生對象的類型,而不是使用catch塊。
TAP
.NET4.0 中引入了新的非同步編程模型“基於任務的非同步編程模型(TAP)”,並且推薦我們在開發新的多線程應用程式中首選TAP,在.NET4.5中更是對TPL庫進行了大量的優化與改進(async和await)。那現在我先介紹下TAP具有哪些優勢:
- 任務調度器(TaskScheduler)依賴於底層的線程池引擎,可自定義一個TaskScheduler更改調度演算法,同時不更改代碼或編程模型。通過局部隊列的任務內聯化(task inlining)和工作竊取(work-stealing)機制而發起了大量任務,Task可以為我們提升程式性能。
- 可以使用PreferFairness標誌,獲取與ThreadPool.QueueUserWorkItem或者一個委托的BeginInvoke相同的線程池行為。
3. 輕鬆實現任務等待、任務取消、延續任務、異常處理(System.AggregateException)、GUI線程操作。
4. 在任務啟動後,可以隨時以任務延續的形式註冊回調。
5. 充分利用現有的線程,避免創建不必要的額外線程。
6. 結合C#5.0引入async和await關鍵字輕鬆實現“非同步方法”。
APM轉換為TAP:
使用TaskFactory的FromAsync方法,傳遞四個實參:BeginXxx方法、EndXxx方法、Object狀態、可選的TaskCreationOptions值,返回對一個Task對象的引用;
private static void ConvertingApmToTask() { // Instead of this: WebRequest webRequest = WebRequest.Create("http://Wintellect.com/"); webRequest.BeginGetResponse(result => { WebResponse webResponse = null; try { webResponse = webRequest.EndGetResponse(result); Console.WriteLine("Content length: " + webResponse.ContentLength); } catch (WebException we) { Console.WriteLine("Failed: " + we.GetBaseException().Message); } finally { if (webResponse != null) webResponse.Close(); } }, null); Console.ReadLine(); // for testing purposes // Make a Task from an async operation that FromAsync starts webRequest = WebRequest.Create("http://Wintellect.com/"); var t1 = Task.Factory.FromAsync<WebResponse>(webRequest.BeginGetResponse, webRequest.EndGetResponse, null, TaskCreationOptions.None); var t2 = t1.ContinueWith(task => { WebResponse webResponse = null; try { webResponse = task.Result; Console.WriteLine("Content length: " + webResponse.ContentLength); } catch (AggregateException ae) { if (ae.GetBaseException() is WebException) Console.WriteLine("Failed: " + ae.GetBaseException().Message); else throw; } finally { if (webResponse != null) webResponse.Close(); } }); try {t2.Wait(); // for testing purposes only} catch (AggregateException) { } }
EAP轉換成TAP:
使用System.Threading.Tasks.TaskCompletionSource類進行包裝;
當構造一個TaskCompletionSource對象,也會生成一個Task,可通過其Task屬性獲取;當一個非同步操作完成時,它使用TaskCompletionSource對象來設置它因為什麼而完成,取消,未處理的異常或者它的結果。調用某個SetXxx方法,可以設置底層Task對象的狀態。
private sealed class MyFormTask : System.Windows.Forms.Form { protected override void OnClick(EventArgs e) { // The System.Net.WebClient class supports the Event-based Asynchronous Pattern WebClient wc = new WebClient(); // Create the TaskCompletionSource and its underlying Task object var tcs = new TaskCompletionSource<String>(); // When a string completes downloading, the WebClient object raises the // DownloadStringCompleted event which will invoke our ProcessString method wc.DownloadStringCompleted += (sender, ea) => { // This code always executes on the GUI thread; set the Task’s state if (ea.Cancelled) tcs.SetCanceled(); else if (ea.Error != null) tcs.SetException(ea.Error); else tcs.SetResult(ea.Result); }; // Have the Task continue with this Task that shows the result in a message box // NOTE: The TaskContinuationOptions.ExecuteSynchronously flag is required to have this code // run on the GUI thread; without the flag, the code runs on a thread pool thread tcs.Task.ContinueWith(t => { try { System.Windows.Forms.MessageBox.Show(t.Result);} catch (AggregateException ae) { System.Windows.Forms.MessageBox.Show(ae.GetBaseException().Message); } }, TaskContinuationOptions.ExecuteSynchronously); // Start the asynchronous operation (this is like calling a BeginXxx method) wc.DownloadStringAsync(new Uri("http://Wintellect.com")); base.OnClick(e); } }
實現了TAP的類:存在XxxTaskAsync的方法, 支持非同步操作的取消和進度的報告的功能;
取消:可以通過協作式取消模式,向非同步方法傳入CancellationToken 參數,通過調用其ThrowIfCancellationRequested方法來定時檢查操作是否已經取消;
進度報告:可以通過IProgress<T>介面來實現進度報告的功能;
更新GUI: TaskScheduler.FromCurrentSynchronizationContext()獲取同步上下文任務調度器,將關聯該對象的所有任務都調度給GUI線程,使任務代碼能成功更新UI;
private sealed class MyForm : System.Windows.Forms.Form { public MyForm() { Text = "Synchronization Context Task Scheduler Demo"; Visible = true; Width = 400; Height = 100; } private static Int32 Sum(CancellationToken ct, Int32 n) { Int32 sum = 0; for (; n > 0; n--) { // The following line throws OperationCanceledException when Cancel // is called on the CancellationTokenSource referred to by the token ct.ThrowIfCancellationRequested(); //Thread.Sleep(0); // Simulate taking a long time checked { sum += n; } } return sum; } private readonly TaskScheduler m_syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); private CancellationTokenSource m_cts; protected override void OnMouseClick(System.Windows.Forms.MouseEventArgs e) { if (m_cts != null) { // An operation is in flight, cancel it m_cts.Cancel(); m_cts = null; } else { // An operation is not in flight, start it Text = "Operation running"; m_cts = new CancellationTokenSource(); // This task uses the default task scheduler and executes on a thread pool thread var t = new Task<Int32>(() => Sum(m_cts.Token, 20000), m_cts.Token); t.Start(); // These tasks use the synchronization context task scheduler and execute on the GUI thread t.ContinueWith(task => Text = "Result: " + task.Result, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, m_syncContextTaskScheduler); t.ContinueWith(task => Text = "Operation canceled", CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled, m_syncContextTaskScheduler); t.ContinueWith(task => Text = "Operation faulted", CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, m_syncContextTaskScheduler); } base.OnMouseClick(e); } }
異常處理
在任務拋出的未處理異常都封裝在System.AggregateException對象中。這個對象會存儲在方法返回的Task或Task<TResult>對象中,需要通過訪問Wait()、Result、Exception成員才能觀察到異常。(所以,在訪問Result之前,應先觀察IsCanceled和IsFaulted屬性)
假如一直不訪問Task的Wait()、Result、Exception成員,那麼你將永遠註意不到這些異常的發生。為了幫助你檢測到這些未處理的異常,可以向TaskScheduler對象的UnobservedTaskException事件註冊回調函數。每當一個Task被垃圾回收時,如果存在一個沒有註意到的異常,CLR的終結器線程會引發這個事件。
可在事件回調函數中調用UnobservedTaskExceptionEventArgs對象的SetObserved() 方法來指出已經處理好了異常,從而阻止CLR終止線程。然而並不推薦這麼做,寧願終止進程也不要帶著已經損壞的狀態繼續運行。
Async /Await
在.NET Framework 4.0中添加.NET Framework 4.5中新的非同步操作庫(async/await),該包由三個庫組成:Microsoft.Bcl、Microsoft.Bcl.Async和Microsoft.Bcl.Build。
Install-Package Microsoft.Bcl.Async
註:asp.net 框架必須要升級.net framework框架才能使用 async/await
C# 5引入了非同步函數(asynchrnous function)的概念。通常是指用async修飾符聲明的,可
包含await表達式的方法或匿名函數;
async關鍵字創建了一個狀態機,類似於yield return語句;await關鍵字只能用於有用async修飾符聲明的方法。async修飾符只能用於返回Task/Task<TResult>或void的方法。await只能用來調用返回Task/Task<TResult>的方法;await會解除線程的阻塞,完成調用的任務;等待任務完成後,獲取結果,然後執行await關鍵字後面的代碼;編譯器會把await的表達式後的代碼使用 Task.ContinueWith 包裝了起來,回調時預設使用當前線程的同步上下文任務調度器;如果不使用相同的同步上下文,必須調用Task實例的ConfigureAwait(false)方法;
await msg.Content.ReadAsStringAsync().ConfigureAwait(false);
非同步方法的聲明語法與其他方法完全一樣,只是要包含async上下文關鍵字。async可以出
現在返回類型之前的任何位置。async修飾符在生成的代碼中沒有作用,也可省略不寫,它明確表達了你的預期,告訴編譯器可以主動尋找await表達式,也可以尋找應該轉換成非同步調用和await表達式的塊調用。
調用者和非同步方法之間是通過返回值來通信的。非同步函數的返回類型只能為:
Void 、Task、Task<TResult>;Task和Task<TResult>類型都表示一個可能還未完成的操作。 Task<TResult>繼承自Task。二者的區別是,Task<TResult>表示一個返回值為T類型的操作,而Task則不需要產生返回值。在某種意義上,你可以認為Task就是Task<void>類型;
之所以將非同步方法設計為可以返回void,是為了和事件處理程式相容。
非同步方法簽名的約束:所有參數都不能使用out或ref修飾符。