C#異步編程
在使用多線程編寫端口掃描程序時,我自己感覺同步和確定所有線程都執行完的時間是2個比較麻煩的問題。有園友評論說現在已經不手動創建thread對象了,而是直接使用Task異步方式,我的網絡編程老師也講到了異步編程的優越性。在學習了課本上的知識后,進行了一個總結分享給大家。從.NET4.5開始,用async和await關鍵字再加上Task.Run是一個非常不錯的異步編程模型。
1.await和async
異步模式從技術上看就是利用委托來實現的,它的主要好處是在異步執行的過程中,用戶仍然可以操控UI界面。使用Task類和使用Thread類有很多相似的地方,Task類也是通過調用方法去實現一個任務的完成,方法可是是命名方法或匿名方法,在執行過程中可使用async和await來實現異步執行。async是一個修飾符,它只能用在方法或者事件處理程序的簽名中。對于方法可分為有返回值和無返回值兩種情況,事件則只有一種,如下面三條語句所示:
private async Task<int> MethodAsync();//有返回值的異步方法
private async Task MethodAsync();//無返回值的異步方法
private async void btnOk_Click();//異步事件處理程序
await是一個運算符,它表示等待異步執行的結果。也可以理解為await運算符實際上是對方法的返回值進行操作,也就是對Task<Result>進行操作,而不是對方法本身進行操作。還有一點要注意,await是一定要放在異步方法的內部,如果沒有放在內部的話,VS會自動報錯。以下是async和await使用的例子:
private async void button5_Click(object sender, EventArgs e)
{
Task a = Method1Async();
//此處可繼續執行其他代碼
await a;//等待任務a完成
Task<int> b = Method2Async();
//此處可繼續執行其他代碼
int c = await b;//等待任務b完成,且可以拿到任務b的返回值
}
Task Method1Async();
async Task<int> Method2Async()
{
await Task.Delay(100);
return 1;
}
await和同步編程最大的不同之處是:異步等待任務完成的時候,在不會繼續執行后面的代碼時,也不會影響界面的操作。在.NET提供的類中,異步方法都是約定用Async作為后綴,這樣可以很清楚的知道這個方法是異步方法還是同步方法。
2. 創建任務
創建任務也就是將任務與要執行的方法聯系起來,編寫任務執行的方法時,這個方法既可以是同步方法也可以是異步方法,還可以是匿名方法。執行異步方法時,必須用async和Task共同表示沒有返回值的任務,用async和Task<TResult>共同表示返回值為TResult的任務。以下是定義執行任務的方法。
private async void button5_Click(object sender, EventArgs e)
{
//Task.Run方法表示使用默認的任務調度程序在線程池中通過后臺執行指定的任務
//如果不需要自己去調度方法,使用這個方式最方便
await Task.Run(()=>Method1Async());//執行同步方法
int c = await Task.Run(()=>Method2Async());//執行異步方法
await Task.Run(async () => { c = 2; });//執行異步匿名方法
}
void Method1Async();
async Task<int> Method2Async(){...}
Task.Run方法常用的重載形式有以下4種,另外它也是可以用new關鍵字顯示創建任務,但是這種方式用的不多。
Task Run(Func<Task> function);//執行不帶返回值的任務
Task<TResult> Run<TResult>(Func<Task<TResult>> function);//執行帶返回值的任務
Task<TResult> Run<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken);//執行過程中可以監聽取消通知
Task Run(Func<Task> function, CancellationToken cancellationToken);//執行過程中可以監聽取消通知
3. 終止任務
在執行任務時肯定會出現需要終止任務的情況,這里的終止告訴任務你要盡快停下來不再執行了,而不是直接銷毀任務實例。這里可以打個比方,學生一起出去吃飯了,學生與老師都在班群里面,突然班群里老師說要讓同學們集合,如果所有同學都看到這個消息,然后學生們開始出發,這樣就可以正確的集合。上面的例子有一個很重要的前提,那就是所有同學都要看到這個消息,也就是學生是時刻監聽消息的。CancellationTokenSource類和CancellationToken結構用于實現多線程、線程池和Task任務的取消操作,處理模式與上面的例子相似。創建的班群就是CancellationTokenSource對象,收到的通知就是CancellationToken對象。CancellationTokenSource用于創建取消通知,CancellationToken則用于傳播應取消操作的通知,當調用任務前,可以先創建取消源對象CancellationTokenSource cts=new CancellationTokenSource();,如果希望在30秒后自動發出取消通知,可以傳入參數CancellationTokenSource(TimeSpan.FromSeconds(30));CancellationToken ct=cts.Token;,后一句代碼是拿到取消的通知。CancellationTokenSource還有一個Cancel方法,將這個屬性設為true時,該方法會將所有添加了取消標記的CancellationToken對象的IsCancellationRequested屬性都設置為true,這樣取消通知就傳遞到了正在執行的任務。
任務收到取消通知后,可以選擇兩種方式來終止操作。第一種方式是簡單的從委托返回。這種實現方式類似于在調用任務的代碼中一個bool值來表示取消通知,任務收到后就直接返回了。當采用這種方式時任務狀態的返回值為TaskStatus.RanToCompletion枚舉值,它表示正常完成,而不是TaskStatus.Canceled枚舉值。第二種方式是在代碼里引發OperationCanceledException異常,并將其傳遞到在其上請求了取消的標記,采用這種方式取消的任務會轉換為用Canceled枚舉值表示的狀態。完成引發異常的首選方式是調用ct.ThrowIfCancellationRequestes();。以下是代碼示例,寫了一個winform程序,利用進度條來取消任務。第一個圖是沒有引發異常時,程序退出for循環,執行后面的代碼后返回了,第二張圖是第二種方式,引發了異常后直接跳轉到catch語句塊了。
CancellationTokenSource cts; private async void button3_Click(object sender, EventArgs e) { progressBar1.Maximum = 100; progressBar1.Value = 0; cts = new CancellationTokenSource(); var aa = MYThreadAsync("a", cts.Token); try { await aa; listBox1.Items.Add("await后面"); } catch { if (aa.IsCanceled) listBox1.Items.Add("a取消"); } }private void button4_Click(object sender, EventArgs e) { cts.Cancel(); } public async Task MYThreadAsync(string s, CancellationToken ct) { for (int i = 0; i < 50; i++) { if (ct.IsCancellationRequested) break; //點擊關閉按鈕,IsCancellationRequested就為true,就會退出for循環,這是第一種方式 progressBar1.Value += 2; await Task.Delay(100); ct.ThrowIfCancellationRequested();//這是第二種方式,它會終止任務并且返回catch語句塊里面 } listBox1.Items.Add("任務" + s + "完成了"); }</pre>
4. 獲取任務執行的狀態
在異步編程中,很顯然任務執行的狀態是一個非常重要的參數。在任務的生命周期里,可以通過Status屬性來獲取任務執行的狀態,當任務完成后還可以通過任務屬性知道任務完成的情況。可利用任務實例的Status屬性獲取任務執行的狀態,任務執行的狀態用TaskStatus枚舉表示,以下是TaskStatus的枚舉值:
Created:任務已經初始化,但尚未進入調度計劃
WaitingForActivation:該任務已進入調度計劃,正在等待被調度程序激活
WaitingToRun:該任務已被調度程序激活,但尚未開始執行
Running:該任務正在運行,但尚未完成
RanToCompletion:該任務已經成功完成
Canceled:該任務由于被取消而完成,引發異常或調用方已向該任務的CancellationToken發出信號
Faulted:該任務因為出現未經處理的異常而完成
WaitingForChildrenToComplete:該任務本身已完成,正等待附加的子任務完成
任務完成情況相關的屬性有IsCompleted、IsCanceled和IsFaulted等屬性,從單詞意思上看不難理解它們的意思,其中要注意IsCompleted屬性表示任務是否完成,無論是正常結束還是因為取消或異常而完成都為完成。
5. 任務執行的進度
有時候我們希望讓某些異步操作提供進度通知,以便在界面中顯示異步操作執行的進度,可以用Progress<T>類來得到任務執行的進度。以下是利用方法里的Report方法將方法內變量的值傳回創建任務的事件代碼里,從而更新進度條的值。
CancellationTokenSource cts; private async void button3_Click(object sender, EventArgs e) { progressBar1.Maximum = 100; progressBar1.Value = 0; cts = new CancellationTokenSource(); CancellationToken ct = cts.Token; var pp = new Progress<int>(); pp.ProgressChanged += (s, n) => { progressBar1.Value = n; }; var tt = Task.Run(()=>MYThreadAsync(pp,cts.Token,500),cts.Token); try { await tt; if (tt.Exception == null) listBox1.Items.Add("任務完成"); } catch (Exception ex) { listBox1.Items.Add("異常" + ex.Message); } } private void button4_Click(object sender, EventArgs e) { cts.Cancel(); } public void MYThreadAsync(IProgress<int> progress, CancellationToken ct, int delay) { int p = 0;//進度 while (p < 100 && ct.IsCancellationRequested == false) { p += 1; Thread.Sleep(delay); progress.Report(p);//這個方法將會觸發ProgressChanged事件更新進度條 } }6. 定時完成任務
無論是服務器還是客戶端,都是有定時完成某個任務的需要的。System.Timers.Timer類是一個不錯的定時設置類,這個類可以引發事件,但它默認是在線程池中引發事件,而不是在當前線程中引發事件。Timer類的常用屬性有AutoReset和Interval屬性,AutoReset是獲取或設置一個bool值,該值為true表示每次間隔結束時都引發一次Elapsed事件,false表示僅在首次間隔結束時引發一次該事件。Interval屬性是獲取或設置兩次Elapsed事件的間隔時間,該值必須大于零并小于Int.MaxValue,默認值為100毫秒。 Timer類還有兩個常用方法那就是Start和Stop方法。
還有一個System.Threading.Timer類,它也是在線程池中定時執行任務,它與前一個Timer類的區別是該類不使用事件模型,而是直接通過TimerCallback類型的委托來實現的。該類的構造函數為:Timer(TimerCallback callback,Object state,TimeSpan douTime,TimeSpan period)。callback表示要執行的方法,state表示一個包含回調方法要使用的信息的對象,dueTime是首次調用回調方法之前延遲的時間,period表示每次調用回調方法的時間間隔,-1表示終止。這樣創建對象后,首次到達dueTime延時時間會自動調用一次callback委托,以后每隔period時間間隔調用一次。以下是這兩種方式的運行效果和源代碼。
System.Timers.Timer timer; System.Threading.Timer threadtimer; private void button2_Click(object sender, EventArgs e)//Timers.Timer { progressBar1.Maximum = 100; progressBar1.Value = 0; int pro=0; timer = new System.Timers.Timer(500); timer.AutoReset = true; timer.Elapsed+= (obj, args) => { pro+=5; progressBar1.Value = pro; }; timer.Start(); }private void button5_Click(object sender, EventArgs e) { timer.Stop(); listBox1.Items.Add("第一個已經停止"); }
//Threading.Timer類 private void button1_Click(object sender, EventArgs e) { progressBar2.Maximum = 100; progressBar2.Value = 0; TimeSpan dueTime = new TimeSpan(0, 0, 0, 1); TimeSpan period = new TimeSpan(0, 0, 0, 0, 200); System.Threading.TimerCallback timecall = new TimerCallback((obj) => progressBar2.Value += 5); threadtimer = new System.Threading.Timer(timecall, null, dueTime, period); }
private void button6_Click(object sender, EventArgs e) { threadtimer.Dispose(); listBox1.Items.Add("第二個已經停止"); }</pre>
這篇文章只總結了單個任務的異步執行的基礎,還得繼續學習多任務并行執行。如果有更好的技術或者與企業使用相關的異步技術,希望園友可以提出我繼續學習。