開篇 非同步編程是程式設計的重點也是難點,還記得在剛開始接觸.net的時候,看的是一本c#的Winform實例教程,上面大部分都是教我們如何使用Winform的控制項以及操作資料庫的實例,那時候做的基本都是資料庫的demo,數據量也不大,程式在執行的時候基本上不會出現阻塞的情況。隨著不斷的深入.net,
開篇
非同步編程是程式設計的重點也是難點,還記得在剛開始接觸.net的時候,看的是一本c#的Winform實例教程,上面大部分都是教我們如何使用Winform的控制項以及操作資料庫的實例,那時候做的基本都是資料庫的demo,數據量也不大,程式在執行的時候基本上不會出現阻塞的情況。隨著不斷的深入.net,也開始進入的實戰,在實際的項目,數據量往往都是比較大,特別是在大量的數據入庫以及查詢數據併進行計算的時候,程式的UI界面往往卡死在那裡,發生了阻塞,這時候就需要對計算時間限制的過程進行非同步處理,讓UI線程繼續相應用戶的操作,使得用戶體驗表現比較友好,同時正確的使用非同步編程去處理計算限制的操作和耗時IO操作還能提升的應用程式的吞吐量及性能。由此可見,非同步編程的重要性。
非同步編程在程式設計中也是非常複雜的,稍有不慎,就會使得你的應用程式變得不穩定,出現異常,甚至會奔潰。但是,比較幸運的是,.net提供非常方便的框架來進行非同步編程,在我看來.net中實現非同步有兩種方式,第一種是多線程的方式,第二種是使用非同步函數,其實在非同步函數中使用的還是多線程的技術。接下來就介紹在.net中如何使用多線程和非同步函數來解決計算限制、耗時等這些不友好用戶體驗的問題。 非同步編程中比較關心,也是比較重要的技術點在於,1)當非同步線程在工作完成時如何通知調用線程,2)當非同步線程出現異常的時候該如何處理,3)非同步線程工作的進度如何實時的通知調用線程。4)如何在調用線程中取消正在工作的非同步線程,併進行回滾操作。 一、非同步函數模型 c#中提供非同步函數編程模式,只要是使用委托對象封裝的函數都可以實現該函數的非同步調用,這是因為委托類型有BeginInvoke和EndInvoke這兩個方法來支持非同步調用。 下麵給出一個例子來講解如何使用委托的來實現非同步調用函數。class Program { public delegate void DoWork(); static void Main(string[] args) { DoWork d = new DoWork(WorkPro);//no.1 d.BeginInvoke(null, null);//no.2 for (int i = 0; i < 100; i++)//no.3 { Thread.Sleep(10);//主線程需要做的事 } Console.WriteLine("主線程done"); Console.ReadKey(); } public static void WorkPro() { //做一些耗時的工作 Thread.Sleep(2000); Console.WriteLine("非同步調用結束"); } }程式定義了一個DoWork類型無參無返回值的的委托類型,no.1用WorkPro方法實例化一個DoWork類型的對象d ,no.2通過委托對象d的BeginInvoke(null,null)(下麵將會詳細介紹BeginInvoke函數中兩個參數如何使用)來實現WorkPro函數的非同步調用,這樣就使得no.3主線程所做的for迴圈和WorkPro函數可以同時執行,這樣使得程式的運行效率得到了大幅度的提升。如果程式是同步執行的話,假設WorkPro函數執行需要2秒,for需要1秒,總共執行時間就需要3秒,如果WorkPro是非同步執行的話,那麼整個程式執行完畢只需要2秒就夠了。 ------ 上面這個例子只是簡單演示瞭如何通過委托來實現函數的非同步調用,而沒有傳遞給該非同步函數任何的參數,也不需要獲取該非同步函數的結果。如果主線需要傳遞給該非同步函數一個參數,並且還要在該非同步函數執行完畢之後獲取其執行結果,那應該如何實現呢?
class Program { public delegate int DoWord(int count); static void Main(string[] args) { DoWord d = new DoWord(WorkPro); IAsyncResult r= d.BeginInvoke(1000,null,null);//no.1 int result= d.EndInvoke(r);//no.2 Console.WriteLine(result); for (int i = 0; i < 100; i++)//no.3 { Thread.Sleep(10);//主線程需要做的事 } Console.WriteLine("主線程done"); Console.ReadKey(); } public static int WorkPro(int count) { int sum = 0; //做一些耗時的工作 for (int i = 0; i < count; i++) { sum += i; } return sum; } }我們已經把委托類型改為具有一個int類型的參數和int類型返回值。在這裡解釋一下,每當你的編譯器發現定義了一個委托類型,就會對應的生成一個類型,並且該類型BeginInvoke方法的參數個數也是不同的,本例聲明的委托類型為: public delegate int DoWord(int count); 實際生成的BeginInvoke原型為:IAsyncResult BeginInvoke(int count, AsyncCallBack callback, object @object) 在no.1處還是和第一個例子一樣調用委托,不同的是用IAsyncResult介面的變數接收了非同步調用(並不是非同步函數)的返回狀態,這是方便後面調用EndInvoke方法接受這個非同步函數調用結果而使用的,也可以通過該參數查看非同步函數執行的狀態,該介面有一個IsCompleted的屬性。在no.2處使用d.EndInvoke(r)來接受非同步函數返回值的。必須指出的是,主線程在調用委托的EndInvoke(r)方法時,當非同步函數沒有執行完畢的話,主線程會一直處於阻塞,等待非同步函數執行完畢,獲取返回值之後才執行no.3的for迴圈。這樣就還會導致主線程處於阻塞狀態。 理想的狀態的是,當非同步函數調用完成之後,自動通知任務執行完成。當然委托也能夠做到,這就要使用BeginInvoke方法的後兩個參數啦。看下麵這個例子。
class Program { public delegate int DoWord(int count); static void Main(string[] args) { DoWord d = new DoWord(WorkPro); IAsyncResult r= d.BeginInvoke(100,CallBack ,d);//no.1 for (int i = 0; i < 100; i++) { Thread.Sleep(10);//主線程需要做的事 } Console.WriteLine("主線程done"); Console.ReadKey(); } public static int WorkPro(int count) { int sum = 0; //做一些耗時的工作 for (int i = 0; i < count; i++) { sum += i; Thread.Sleep(10); } return sum; } public static void CallBack(IAsyncResult r) { DoWord d = (DoWord)r.AsyncState; Console.WriteLine("非同步調用完成,返回結果為{0}", d.EndInvoke(r)); } }首先來解釋一下BeginInvoke方法的第二個參數是AsyncCallBack 類型的委托(回調函數),當該參數不為空,那麼在非同步函數執行完畢之後,會調用該委托;第三個參數Object 類型的,代表傳遞給回調函數的非同步調用狀態。CallBack回調函數必須帶有一個IAsyncResult 類型的參數,通過這個參數可以在回調方法內部獲取非同步調用的結果。在no.1出就給BeginInvoke函數傳遞了回調函數CallBack,和委托d,當非同步數WorkPro執行完畢之後,就立即通知CallBack回調函數來顯示執行結果。這下主線程就不需要阻塞一直的等待非同步函數的結果,大大的提升了程式的運行效率。在.net還提供許多類的BeinXXX()和EndXXX()的非同步版本,比如文件的讀寫等,具體可以查閱相關的資料。 其中非同步函數內部所使用的線程均是線程池中的工作線程,由線程池去分配管理的。 二、多線程模型
.net在System.Threading和System.Threading.Tasks這兩個命名空間中提供了Thread,ThreadPool,和Task三個類來處理多線程的問題,其中Thread是建立一個專用線程,ThreadPool是使用線程池中工作線程,而Task類是採用任務的方式,其內部也是使用線程池中的工作線程。本節只講Tread類和Tasks類的使用以及其優劣。
1、Thread類 Thread類的使用方法很簡單,它開闢的是一個專用線程,不是線程池中的工作線程,不由線程池去管理。該類提供4個重載版本,常見的使用前面兩個就好了。 1)public Thread( ThreadStart start ):其中ThreadStart是一個無參無返回值的委托類型。 2)public Thread( ParameterizedThreadStart start ):其中ParameterizedThreadStart 是一個帶有一個Object類型的參數,無返回值的委托類型。 從Thread類提供了兩個構造函數可以看出,Thread類能夠非同步調用無參無返回值的函數,也能夠非同步調用帶一個Object類型的無返回值的函數。下麵就給出一個例子簡單的演示一下如何使用Thread非同步執行一個帶參數的函數。
class Program { static void Main(string[] args) { Thread t = new Thread(WorkPro);//no.1 t.IsBackground = true;//no.2 t.Start(1000);//no.3 } public static void WorkPro(object t) { //做一些耗時的工作 int count=(int)t; for (int i = 0; i < count; i++) { Thread.Sleep(2000); } Console.WriteLine("任務處理完成"); } }
no.1實例化一個Thread對象,給傳入一個ParameterizedThreadStart 類型的委托;no.2將建立的專用線程設置為後臺的任務線程(後臺線程會隨著調用線程(即使任務沒完成)的終止而強制終止,而前臺線程如果任務沒有處理完,是不會隨著調用線程的終止而終止的);no.3調用Start(1000)方法,其中1000是傳遞給非同步執行函數的參數。記住,如果構造Thread對象是ThreadStart委托,那麼Start()就直接調用,否則會出現異常。只需要簡單的幾行代碼就能實現函數的非同步調用。 其中,當非同步函數中處理需要多個參數時,那麼只需要建立一個參數類,參數類中包括你函數需要的參數個數,然後將這個參數類傳遞給非同步函數即可。 Thread類的使用雖然簡單,但是它還是有一定的劣勢的,一般不推薦使用。 1)Thread類創建的是一個專用線程,建立一個專用線程是非常耗用系統的資源,建議是使用線程池中的線程。 2)Thread類不能很好的和調用線程進行交互,當任務完成時不能及時的通知,在調用線程也不能隨時的取消正在進行的任務。 另外在以下情況下,就只能選擇使用Thread類了。 1)執行任務的線程要以非普通的優先順序去執行,因為線程池的線程都是以普通優先順序運行的。 2)執行任務的線程要表現為一個前臺線程,因為線程池的線程始終都是一個後臺線程。 3)非同步執行的任務需要長時間的,那麼就可以使用Thread類為該任務建立一個專用線程。
2、Task類
Task類是封裝的一個任務類,內部使用的是ThreadPool類,提供了內建機制,讓你知道什麼時候非同步完成以及如何獲取非同步執行的結果,並且還能取消非同步執行的任務。下麵看一個例子是如何使用Task類來執行非同步操作的。class Program { static void Main(string[] args) { Task t = new Task((c) => { int count = (int)c; for (int i = 0; i < count; i++) { Thread.Sleep(10); } Console.WriteLine("任務處理完成"); }, 100);//no.1 t.Start(); for (int i = 0; i < 100; i++) { Thread.Sleep(10); } Console.WriteLine("done"); } }no.1處使用Task的構造函數為: public Task( Action<Object> action, Object state )一個Action<Object>類型的委托(即非同步調用函數具有一個Object類型的參數),和一個Object類型的參數,也就是傳遞給非同步函數的參數,Task類還有幾種方式的重載,我們還可以傳遞一些TaskCreationOptions標誌來控制Task的執行方式。在這裡我使用的是lambda表達去寫委托的,這樣使得程式的結構更加的清晰,使用Start()來啟動非同步函數的調用。 -------- 如果需要非同步函數有返回值,那麼此時就需要使用Task<TResult>泛型類(派生自Task)來實現,其中TResult代表返回的類型。因為非同步函數具有返回值,所以Task<TResult>的各種重載版本的構造函數第一個委托類型的參數都是Fun<TResult>或者Fun<Object,TResult>。下麵演示等待任務完成並獲取其結果。
class Program { static void Main(string[] args) { Task<int> t = new Task<int>((c) => { int count = (int)c; int sum=0; for (int i = 0; i < count; i++) { Thread.Sleep(10); sum+=i; } Console.WriteLine("任務處理完成"); return sum; }, 100); t.Start(); t.Wait();//no.1 Console.WriteLine("任務執行的結果{0}", t.Result);//no.2 for (int i = 0; i < 100; i++) { Thread.Sleep(10); } Console.WriteLine("done"); } }如果任務中出現了異常,那麼異常會被吞噬掉,並存儲到一個集合中去,而線程可以返回到線程池中去。但是如果在代碼中調用了Wait方法或者是Result屬性,任務有異常發生就會被引發,不會被吞噬掉。其中Result屬性內部本身也調用了Wati方法。Wait方法和上一節中的委托的EndInvoke方法類似,會使得調用線程阻塞直到非同步任務完成。下麵我們會介紹如何避免獲取非同步結果的阻塞情況,在講解之前,先說一下,如何取消正在運行的任務。 ------ 看下麵一段代碼如何演示取消正在運行的任務。
class Program { static void Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource();//no.1 Task<int> t = new Task<int>((c) =>Sum(cts.Token ,(int)c), 100);//no.2 t.Start(); cts.Cancel();//no.3如果任務還沒完成,但是Task有可能完成啦 for (int i = 0; i < 100; i++) { Thread.Sleep(10); } Console.WriteLine("done"); } static int Sum(CancellationToken ct, int count) { int sum = 0; for (int i = 0; i < count; i++) { if (!ct.CanBeCanceled) { Thread.Sleep(10); sum += i; } else { Console.WriteLine("任務取消"); //進行回滾操作 return -1;//退出任務 } } Console.WriteLine("任務處理完成"); return sum; } }取消任務要引用一個CancellationTokenSource 對象。在需要非同步執行的方法中增加一個CancellationToken類型的形參。然後在非同步函數的for迴圈代碼中用一個if語句判斷CancellationToken的CanBeCanceled屬性,這個屬性可以用來判斷在調用線程是否取消任務的執行,除CanBeCanceled屬性之外,還可以使用ThrowIfCancellationRequested方法,該方法的作用是如果在調用線程調用CancellationTokenSource對象的Cancel方法,那麼就會引發一個異常,然後在調用線程進行捕捉就好了,這是在非同步函數中的處理方式。no.1在構建任務之前需要建立一個CancellationTokenSource ,no2.並且把CancellationTokenSource傳遞給非同步調用函數,傳遞的是CancellationTokenSource對象的Toke屬性,該屬性是一個CancellationToken類型的對象。這樣就完成任務的取消模式,如果想在調用線程中取消任務的執行,只需要調用CancellationTokenSource 的Cancel方法就行啦。 ------ 前面就說過了,獲取任務結果調用Wait方法和Result屬性導致調用線程阻塞,那麼如何處理這種情況呢,這就使用了Task<TResult>類提供的ContinueWith方法。該方法的作用是當任務完成時,啟動一個新的任務,不僅僅是如此,該方法還有可以在任務只出現異常或者取消等情況的時候才執行,只需要給該方法傳遞TaskContinuationOptions枚舉類型就可以了。下麵就演示一下如何使用ContinueWith方法。 首先看下ContinueWith方法的原型。 public Task ContinueWith( Action<Task> continuationAction )採用一個Action<Task>類型的委托。該方法提供了多種重載的版本,這隻是最簡單的一種。
public Task ContinueWith( Action<Task> continuationAction, TaskContinuationOptions continuationOptions )第二個參數代表新任務的執行條件,當任務滿足這個枚舉條件才執行 Action<Task>類型的回調函數。
代碼如下:class Program { static void Main(string[] args) { Task<int> t = new Task<int>((c) =>Sum((int)c), 100); t.Start(); t.ContinueWith(task => Console.WriteLine("任務完成的結果{0}", task.Result));//當任務執行完之後執行 t.ContinueWith(task => Console.WriteLine(""), TaskContinuationOptions.OnlyOnFaulted);//當任務出現異常時才執行 for (int i = 0; i < 200; i++) { Thread.Sleep(10); } Console.WriteLine("done"); } static int Sum( int count) { int sum = 0; for (int i = 0; i < count; i++) { Thread.Sleep(10); sum += i; } Console.WriteLine("任務處理完成"); return sum; } }
t.Start()之後調用第一個ContinueWith方法,該方法第一參數就是一個Action<Task>的委托類型,相當於是一個回調函數,在這裡我也用lambda表達式,當任務完成就會啟用一個新任務去執行這個回調函數。而第二個ContinueWith裡面的回調方法卻不會執行,因為我們的任務也就是Sum方法不會發生異常,不能滿足TaskContinuationOptions.OnlyOnFaulted這個枚舉條件。這種用法比委托的非同步函數編程看起來要簡單些。最關鍵的是ContinueWith的還有一個重載版本可以帶一個TaskScheduler對象參數,該對象負責執行被調度的任務。FCL中提供兩種任務調度器,均派生自TaskScheduler類型:線程池調度器,和同步上下文任務調用器。而在Winform窗體程式設計中TaskScheduler尤為有用,為什麼這麼說呢?因為在窗體程式中的控制項都是有ui線程去創建,而我們所執行的後臺任務使用線程都是線程池中的工作線程,所以當我們的任務完成之後需要反饋到Winform控制項上,但是控制項創建的線程和任務執行的線程不是同一個線程,如果在任務線程中去更新控制項就會導致控制項對象安全問題會出現異常。所以操作控制項,就必須要使用ui線程去操作。因此在ContinueWith獲取任務執行的結果的並反饋到控制項的任務調度上不能使用線程池任務調用器,而要使用同步上下文任務調度器去調度,即採用ui這個線程去調用ContinueWith方法所綁定的回調用函數即Action<Task>類型的委托。下麵將使用任務調度器來把非同步執行的Sum計算結果反饋到Winform界面的TextBox控制項中。
界面如下。
代碼如下。
public partial class Form1 : Form { private readonly TaskScheduler contextTaskScheduler;//聲明一個任務調度器 public Form1() { InitializeComponent(); contextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();//no.1獲得一個上下文任務調度器 } private void button1_Click(object sender, EventArgs e) { Task<int> t = new Task<int>((n) => Sum((int)n),100); t.Start(); t.ContinueWith(task =>this.textBox1 .Text =task.Result.ToString(),contextTaskScheduler);//當任務執行完之後執行 t.ContinueWith(task=>MessageBox .Show ("任務出現異常"),CancellationToken.None ,TaskContinuationOptions.OnlyOnFaulted,contextTaskScheduler );//當任務出現異常時才執行 } int Sum(int count) { int sum = 0; for (int i = 0; i < count; i++) { Thread.Sleep(10); sum += i; } Console.WriteLine("任務處理完成"); return sum; } }在no.1窗體的構造函數獲取該UI線程的同步上下文調度器。在按鈕的事件接受非同步執行的結果時候,都傳遞了contextTaskScheduler同步上下文的調度器,目的是,當非同步任務完成之後,調度UI線程去執行任務完成之後的回調函數。 ------ 到目前為止,我平常用到的非同步編程模式也就這麼多了,當然Task類的ContinueWith還有很多重載的版本,會提供不一樣效果。在開篇的時候就說,如何在調用線程中實時獲取非同步任務的執行情況,比如我的任務是插入100w條數據到資料庫,我在界面中需要實時的刷新數據導入的進度條,這種情況使用上述所講的是做不到的。具體如何做到,我在另外一篇文章已經詳細的講過啦,採用回調函數的方法(委托)來實現,鏈接:http://www.cnblogs.com/mingjiatang/p/5079632.html。 三、小結 雖然在.net中提供了眾多的非同步編程模式,但是推薦最好使用Task類,因為Task類使用線程池中的任務線程,又由線程池管理,效率相對來說較高,而且Task類內部有比較好的機制,能讓調用線程與任務進行交互。反正不管用哪種模式,總之儘量不要出現阻塞的情況,只要程式中出現線程阻塞,線程池就會創建新的活動線程,因為線程池總是要保證活動的任務線程數量與CPU的核數一致,它覺得這樣性能最佳,當阻塞的線程恢復正常之後,線程池又會將多餘的線程銷毀,避免系統調度線程時頻繁的進行上下文切換。這樣的創建、銷毀線程是非常的浪費系統資源影響性能的。而線上程同步的時候常常會出現阻塞的情況,所以能設計不用線程同步去解決問題,儘量不用線程同步。最後要是有寫的不對的地方,請各位指正,謝謝!