Ø 前言
C# Asynchronous Programming(異步程式設計)有幾種實作方式,其中 Asynchronous Method(異步方法)就是其中的一種。異步方法是 C#5.0 才有的新特性,主要采用 async、await 關鍵字聲明為異步方法,完成對方法的異步調用。C#5.0 對應的 VS 版本是 VS2012,對應的 .NET Framework 版本是 v4.5,是以需要在此基礎上才支援。(否則可能報:找不到“async”修飾符所需的所有類型。目标架構版本是否不正确,或者缺少對程式集的引用?)
Ø 什麼是異步方法
1. 異步方法,是指在執行目前方法的同時,可以異步的去調用其他方法(異步方法),并且不會阻塞目前方法的線程。
2. 使用了 async 修飾符的方法稱為異步方法,通常配合 await 運算符和 Task 異步任務一起使用。
1) 如果方法使用了 async 修飾符,則方法中需要包含一個以上 await 運算符,否則将以同步執行。
2) 反之,如果方法中包含一個以上 await 運算符,則必須聲明為一個異步方法,即使用 async 修飾符。
3. Task 分為兩種:
1) Task,表示可以執行一個異步操作,聲明如下:
public class Task : IAsyncResult, IDisposable { }
2) Task<TResult>,表示可以執行帶有傳回值的異步操作,聲明如下:
public class Task<TResult> : Task { }
4. 異步方法的傳回類型必須為 void、Task、Task<TResult> 中的其中一種。
1) void,表示無傳回值,不關心異步方法執行後的結果,一般用于僅僅執行某一項任務,但是不關心結果的場景。
2) Task,表示異步方法将傳回一個 Task 對象,該對象通常用于判斷異步任務是否已經完成,可以使用 taskObj.Wait() 方法等待,或者 taskObj.IsCompleted 判斷。
3) Task<TResult>,表示異步方法将傳回一個 Task<TResult> 對象,該對象的 Result 屬性則是異步方法的執行結果,調用該屬性時将阻塞目前線程(異步方法未執行完成時)。
Ø 歸納一下:void 不關心結果;Task 隻關心是否執行完成;Task<TResult> 不止關心是否執行完成,還要擷取執行結果。
Ø 下面通過幾個生活中比較形象的例子來了解異步方法的使用
1. 模拟扔垃圾(不關心結果,傳回 void 類型)
/// <summary>
/// 扔垃圾。
/// </summary>
public void DropLitter()
{
WriteLine("老婆開始打掃房間,線程Id為:{0}", GetThreadId());
WriteLine("垃圾滿了,快去扔垃圾");
CommandDropLitter();
WriteLine("不管他繼續打掃,線程Id為:{0}", GetThreadId());
Thread.Sleep(100);
WriteLine("老婆把房間打掃好了,線程Id為:{0}", GetThreadId());
}
/// 通知我去扔垃圾。
public async void CommandDropLitter()
WriteLine("這時我準備去扔垃圾,線程Id為:{0}", GetThreadId());
await Task.Run(() =>
{
WriteLine("屁颠屁颠的去扔垃圾,線程Id為:{0}", GetThreadId());
Thread.Sleep(1000);
});
WriteLine("垃圾扔了還有啥吩咐,線程Id為:{0}", GetThreadId());
運作以上代碼:
以上代碼在 CommandDropLitter() 方法上加了 async 修飾符,并且使用 await 運算符開啟了一個新的 Task 去執行另一個任務。注意:目前線程遇到 await 時,則立刻跳回調用方法繼續往下執行。而 Task 執行完成之後将執行 await 之後的代碼,并且與 await 之前的線程不是同一個。
2. 模拟打開電源開關(關心是否執行完成,傳回 Task 類型)
/// 打開電源開關。
public void OpenMainsSwitch()
WriteLine("我和老婆正在看電視,線程Id為:{0}", GetThreadId());
WriteLine("突然停電了,快去看下是不是跳閘了");
Task task = CommandOpenMainsSwitch();
WriteLine("沒電了先玩會兒手機吧,線程Id為:{0}", GetThreadId());
WriteLine("手機也沒電了隻等電源打開,線程Id為:{0}", GetThreadId());
//task.Wait(); //是以這裡将被阻塞,直到任務完成
//或者
while (!task.IsCompleted) { Thread.Sleep(100); }
WriteLine("又有電了我們繼續看電視,線程Id為:{0}", GetThreadId());
/// 通知我去打開電源開關。
public async Task CommandOpenMainsSwitch()
WriteLine("這時我準備去打開電源開關,線程Id為:{0}", GetThreadId());
WriteLine("屁颠屁颠的去打開電源開關,線程Id為:{0}", GetThreadId());
WriteLine("電源開關打開了,線程Id為:{0}", GetThreadId());
1) 可見,調用 Wait() 方法後,目前線程被阻塞了,直到 Task 執行完成後,目前線程才繼續執行。
2) 注意:由于 CommandOpenMainsSwitch() 是一個異步方法,雖然傳回類型為 Task 類型,但是在我們代碼中并沒有寫(也不能寫) return task 語句,這是為什麼呢?可能是這種傳回類型比較特殊,或者編譯器自動幫我們完成了吧!就算寫也隻能寫 return 語句,後面不能跟對象表達式。
3. 模拟去買鹽(不止關心是否執行完成,還要擷取執行結果。傳回 Task<TResult> 類型)
/// 做飯。
public void CookDinner()
WriteLine("老婆開始做飯,線程Id為:{0}", GetThreadId());
WriteLine("哎呀,沒鹽了");
Task<string> task = CommandBuySalt();
WriteLine("不管他繼續炒菜,線程Id為:{0}", GetThreadId());
string result = task.Result; //必須要用鹽了,等我把鹽回來(停止炒菜(阻塞線程))
WriteLine("用了鹽炒的菜就是好吃【{0}】,線程Id為:{1}", result, GetThreadId());
WriteLine("老婆把飯做好了,線程Id為:{0}", GetThreadId());
/// 通知我去買鹽。
public async Task<string> CommandBuySalt()
WriteLine("這時我準備去買鹽了,線程Id為:{0}", GetThreadId());
string result = await Task.Run(() =>
WriteLine("屁颠屁颠的去買鹽,線程Id為:{0}", GetThreadId());
return "鹽買回來了,順便我還買了一包煙";
WriteLine("{0},線程Id為:{1}", result, GetThreadId());
return result;
1) 以上代碼 task.Result 會阻塞目前線程,與 task.Wait() 類似。
2) 注意:與前面傳回類型為 Task 的 CommandOpenMainsSwitch() 方法一樣,雖然 CommandBuySalt() 方法傳回類型為 Task<string>,但是我們的傳回語句是 return 字元串。
Ø 其他示例
1. 在前面(模拟去買鹽)的示例中,異步方法中隻開啟了一個 Task,如果開啟多個 Task 又是什麼情況,看代碼:
public void AsyncTest()
WriteLine("AsyncTest() 方法開始執行,線程Id為:{0}", GetThreadId());
Task task = Test1();
WriteLine("AsyncTest() 方法繼續執行,線程Id為:{0}", GetThreadId());
task.Wait();
WriteLine("AsyncTest() 方法結束執行,線程Id為:{0}", GetThreadId());
public async Task Test1()
WriteLine("Test1() 方法開始執行,線程Id為:{0}", GetThreadId());
await Task.Factory.StartNew((state) =>
WriteLine("Test1() 方法中的 {0} 開始執行,線程Id為:{1}", state, GetThreadId());
WriteLine("Test1() 方法中的 {0} 結束執行,線程Id為:{1}", state, GetThreadId());
}, "task1");
Thread.Sleep(3000);
}, "task2");
WriteLine("Test1() 方法結束執行,線程Id為:{0}", GetThreadId());
當異步方法中有多個 await 時,會依次執行所有的 Task,隻有當所有 Task 執行完成後才表示異步方法執行完成,目前線程才得以執行。
2. 同樣以前面(模拟去買鹽)的示例,如果發現其實家裡還有鹽,這是就要告訴我不用買了(取消異步操作),怎麼實作?這就要借助 System.Threading.CancellationTokenSource 和 System.Threading.Tasks.CancellationToken 對象來完成。
/// 做飯(買鹽任務取消)。
public void CookDinner_CancelBuySalt()
CancellationTokenSource source = new CancellationTokenSource();
Task<string> task = CommandBuySalt_CancelBuySalt(source.Token);
string result = "家裡的鹽";
if (!string.IsNullOrEmpty(result))
source.Cancel(); //傳達取消請求
WriteLine("家裡還有鹽不用買啦,線程Id為:{0}", GetThreadId());
}
else
//如果已取消就不能再獲得結果了(否則将抛出 System.Threading.Tasks.TaskCanceledException 異常)
//你都叫我不要買了,我拿什麼給你?
result = task.Result;
WriteLine("既然有鹽我就繼續炒菜【{0}】,線程Id為:{1}", result, GetThreadId());
WriteLine("最終的任務狀态是:{0},已完成:{1},已取消:{2},已失敗:{3}",
task.Status, task.IsCompleted, task.IsCanceled, task.IsFaulted);
/// 通知我去買鹽(又告訴我不用買了)。
public async Task<string> CommandBuySalt_CancelBuySalt(CancellationToken token)
//已開始執行的任務不能被取消
}, token).ContinueWith((t) => //若沒有取消就繼續執行
WriteLine("鹽已經買好了,線程Id為:{0}", GetThreadId());
}, token);
1) 剛開始我以為調用 source.Cancel() 方法後會立即取消 Task 的執行,仔細一想也不太可能。如果需要在 Task 執行前或者執行期間完成取消操作,我們自己寫代碼判斷 cancellationToken.IsCancellationRequested 屬性是否為 true(該屬性在調用 source.Cancel() 後或者 source.CancelAfter() 方法到達指定時間後為 true),如果為 true 結束執行即可。
2) 這裡所說的“傳達取消請求”的意思是,每個 Task 在執行之前都會檢查 cancellationToken.IsCancellationRequested 屬性是否為 true,如果為 true 則不執行 Task,并将設定 Status、IsCompleted、IsCanceled 等。
3) 是以,在 Task 的源碼中有這樣一段代碼
if (cancellationToken.IsCancellationRequested)
// Fast path for an already-canceled cancellationToken
this.InternalCancel(false);
4) 官網示例:CancellationTokenSource 類 (System.Threading)
3. 乘熱打鐵,我們再來看看多個 CancellationTokenSource 取消異步任務,以及注冊取消後的回調委托方法,繼續以(模拟去買鹽)為例:
/// 做飯(多個消息傳達買鹽任務取消)。
public void CookDinner_MultiCancelBuySalt()
CancellationTokenSource source1 = new CancellationTokenSource(); //因為存在而取消
CancellationTokenSource source2 = new CancellationTokenSource(); //因為放棄而取消
CancellationTokenSource source = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token);
//注冊取消時的回調委托
source1.Token.Register(() =>
WriteLine("這是因為{0}是以取消,線程Id為:{1}", "家裡還有鹽", GetThreadId());
source2.Token.Register((state) =>
WriteLine("這是因為{0}是以取消,線程Id為:{1}", state, GetThreadId());
}, "不做了出去吃");
source.Token.Register((state) =>
}, "沒理由");
//這裡必須傳遞 CancellationTokenSource.CreateLinkedTokenSource() 方法傳回的 Token 對象
Task<string> task = CommandBuySalt_MultiCancelBuySalt(source.Token);
WriteLine("等等,好像不用買了,線程Id為:{0}", GetThreadId());
string[] results = new string[] { "家裡的鹽", "不做了出去吃", "沒理由" };
Random r = new Random();
switch (r.Next(1, 4))
case 1:
source1.Cancel(); //傳達取消請求(家裡有鹽)
//source1.CancelAfter(3000); //3s後才調用取消的回調方法
WriteLine("既然有鹽我就繼續炒菜【{0}】,線程Id為:{1}", results[0], GetThreadId());
break;
case 2:
source2.Cancel(); //傳達取消請求(不做了出去吃)
//source2.CancelAfter(3000); //3s後才調用取消的回調方法
WriteLine("我們出去吃不用買啦【{0}】,線程Id為:{1}", results[1], GetThreadId());
case 3:
source.Cancel(); //傳達取消請求(沒理由)
//source.CancelAfter(3000); //3s後才調用取消的回調方法
WriteLine("沒理由就是不用買啦【{0}】,線程Id為:{1}", results[2], GetThreadId());
/// 通知我去買鹽(又告訴我不用買了,各種理由)。
public async Task<string> CommandBuySalt_MultiCancelBuySalt(CancellationToken token)
1) 當調用 source.Cancel() 方法後,會立即取消并調用 token 注冊的回調方法;而調用 existSource.CancelAfter() 方法則會等到達指定的毫秒數後才會取消。
2) 注意:傳遞給異步方法的 token 對象,必須是 CancellationTokenSource.CreateLinkedTokenSource() 方法傳回的 Token 對象,否則取消将無效。
3) 回調的委托方法始終隻有兩個,一個是 CancellationTokenSource.CreateLinkedTokenSource() 方法傳回的 Token 對象的注冊委托,另一個是調用 Cancel()/CancelAfter() 方法的 Token 對象的注冊委托。
4) 如果以上代碼調用的是 CancelAfter(3000) 方法,運作結果如下: