之前曾經介紹過如何使用 Thread 或 ThreadPool 建立執行緒,以達到多個工作同步執行的目的。 並利用「同步處理」的機制,以取得非同步作業的執行結果。 自 .NET 4.0 開始,在 System.Threading.Tasks 命名空間中,已提供幾個相關的 Task 類別,可以用來簡化撰寫並行作業的程式碼。 這一組相關的類別庫就稱為「工作平行程式庫」( ,TPL),它簡化了撰寫平行作業的許多細節,讓開發人員可以更專注於程式所應完成的工作。
- System.Threading.Tasks.Task :表示非同步作業。
- System.Threading.Tasks.Task<TResult> :表示含有傳回值的非同步作業。
Task
Task 類別 & Task<TResult> 類別
Task 類別
這個類別用來表示一個無回傳值的非同步作業。 底下幾個是它的建構函式,其中 Action 表示一個沒有回傳值的委派。
Task(Action) Task(Action<Object>, Object) Task(Action, CancellationToken) Task(Action, TaskCreationOptions) Task(Action<Object>, Object, CancellationToken, TaskCreationOptions) ...
Task<TResult> 類別
這個 Task<TResult> 類別表示一個有回傳值的非同步作業。 所以和上面 Task 類別,差別只在於其建構函式中傳入的是一個可回傳值的委派。
Task<TResult>(Func<TResult>): Task<TResult>(Func<Object, TResult>, Object): Task<TResult>(Func<TResult>, CancellationToken): Task<TResult>(Func<TResult>, TaskCreationOptions): Task<TResult>(Func<Object, TResult>, Object, CancellationToken, TaskCreationOptions): ...
Task<TResult>(Func<TResult>)
這個建構函式表示:
- 這是一個泛型類別,類別中會使用到一個非特定型別(TResult)。
- 具現化時,必須使用一個無參數的 Func 委派當做參數。
- 委派的執行函式必須回傳(TResult)型別的值。
private AtomEntry UploadPhoto1() { return UploadPhotoAsync(gsAlubmId, @"01.jpg"); //return AtomEntry; } private void bnTaskResult_Click(object sender, EventArgs e) { // 建立 Task<TResult>(Func<TResult>) Task<AtomEntry> task = new Task<AtomEntry>( delegate () { return UploadPhoto1(); } ); // 上面程式碼,可簡寫如下: Task<AtomEntry> task = new Task<AtomEntry>(UploadPhoto1); }
Task<TResult>(Func<Object, TResult>, Object)
這個建構函式表示:
- 這是一個泛型類別,類別中會使用到一個非特定型別(TResult)。
- 具現化時,必須使用一個含一個參數的 Func 委派當做參數。
- 委派的執行函式必須傳入一個 Object 型別參數,並回傳(TResult)型別的值。
private AtomEntry UploadPhoto2(Object filename) { return UploadPhotoAsync(gsAlubmId, filename.ToString()); //return AtomEntry; } private void bnTaskResult_Click(object sender, EventArgs e) { // Task<TResult>(Func<Object, TResult>, Object) Task<AtomEntry> task2 = new Task<AtomEntry>(UploadPhoto2, filename); }
起始非同步工作
Task 類別可用來簡化 Thread 或 ThreadPool 執行非同步工作的邏輯,你可以先建立 Task 執行個體,再叫用 Task.Start 方法來啟動非同步工作;或者使用靜態方法 TaskFactory.StartNew 直接建立 Task 並執行非同步工作。
Task.Start()
底下範例會自動建立多個Thread執行上傳動作。
private void UploadPhoto0(Object filename) { UploadPhotoAsync(gsAlubmId, filename.ToString()); } private void bnTask2_Click(object sender, EventArgs e) { FileInfo[] files = GetFileList(); foreach (FileInfo file in files) { string filename = file.FullName; Task task = new Task(UploadPhoto0, filename); task.Start(); } Console.WriteLine("Finish"); }
如果你要等待所有非同步工作執行結束才繼續往下的話,可以叫用 Task.WaitAll 方法。
private void bnTask2_Click(object sender, EventArgs e) { FileInfo[] files = GetFileList(); List<Task> tasks = new List<Task>(); foreach (FileInfo file in files) { string filename = file.FullName; Task task = new Task(UploadPhoto0, filename); task.Start(); } Task.WaitAll(tasks.ToArray()); //主執行緒會停留在這一行,直到所有 Task 執行完畢,才會繼續往下。 Console.WriteLine("Finish"); }
由於 Task 建構函式中是一個 Action 委派參數,所以我們也可以使用 Lambda 運算式和匿名方法來建立委派。
FileInfo[] files = GetFileList(); List<Task> tasks = new List<Task>(); foreach (FileInfo file in files) { string filename = file.FullName; Task task = new Task( () => { UploadPhotoAsync(gsAlubmId, filename); }); task.Start(); tasks.Add(task); } Task.WaitAll(tasks.ToArray()); //主執行緒會停留在這一行,直到所有 Task 執行完畢,才會繼續往下。 Console.WriteLine("Finish");
TaskFactory.StartNew()
TaskFactory 類別是 Task 類別的輔助工具,提供 Task 物件操作的便利性。 例如,你可以直接叫用其 StartNew 方法,就可以建立 Task 執行個體,並啟動該並行作業。 所以,上面的例子,可以改寫如下:
FileInfo[] files = GetFileList(); foreach (FileInfo file in files) { string filename = file.FullName; Task task = Task.Factory.StartNew(UploadPhoto0, filename); } Console.WriteLine("Finish");
Task.Run()
取得非同步工作的執行結果
雖然 Task 類別本身無法回傳執行結果, 但是它的泛型類別 Task<TResult> 可以。 當使用 Task<TResult> 類別建立非同步工作時,若要取得執行結果,可以直接由執行個體的 Result 屬性取得。 不過,要取得執行結果,你必須先等待工作執行結束,因此在程式碼中必須加入等待結束的機制。
- Task.Wait :等候該 Task 完成執行。
- Task.WaitAny :這個靜態方法,會等候任一提供的 Task 物件完成執行。
- Task.WaitAll :這個靜態方法,會等候所有提供的 Task 物件完成執行。
底下這個例子,在 Task 啟動後,便叫用 Wait 方法等待該 Task 執行完畢才繼續往下。
private AtomEntry UploadPhoto2(Object filename) { return UploadPhotoAsync(gsAlubmId, filename.ToString()); } private void bnTaskResult_Click(object sender, EventArgs e) { FileInfo[] files = GetFileList(); foreach (FileInfo file in files) { string filename = file.FullName; Task<AtomEntry> task = new Task<AtomEntry>(UploadPhoto2, filename); task.Start(); task.Wait(); //等待並行作業執行完畢,再繼續往下。 Photo photo = new Photo(); photo.AtomEntry = task.Result; //讀取非同步工作的執行結果 Console.WriteLine("ID:{0} URI:{1}", photo.Id, photo.PhotoUri); } }
上面範例中,其實在 Task 作業中,只要該 Task 有叫用到 Result 屬性,既使沒有呼叫 Wait 方法,程式碼也會停在該行程式,直到該並行作業執行完畢才續繼往下。
另外的做法,你也可以先啟用所有的 Task ,再叫用 WaitAll 方法,等待所有並行的作業執行完畢。 不過這個方法無法確保每個 Task 的執行順序,如果在意每個工作先後完成的順序,就只能使用上面方法。
FileInfo[] files = GetFileList(); List<Task> tasks = new List<Task>(); foreach (FileInfo file in files) { string filename = file.FullName; Task<AtomEntry> task = new Task<AtomEntry>(UploadPhoto2, filename); task.Start(); tasks.Add(task); } Task.WaitAll(tasks.ToArray()); //主程式會停留在這一行,直到所有 Task 執行完畢,才會繼續往下。 foreach (Task<AtomEntry> task in tasks) { Photo photo = new Photo(); photo.AtomEntry = task.Result; Console.WriteLine("ID:{0} URI:{1}", photo.Id, photo.PhotoUri); }
Task.ContinueWith
如果在 Task<TResult> 作業執行結束後,你希望接著執行另一段程式,可以使用 ContinueWith 。它的多載方法有:
Task ContinueWith(Action<Task<TResult>> continuationAction)
這個方法表示:
- ContinueWith 會回傳一個 Task 型別值。
- ContinueWith 的參數是一個 Action 委派。
- 該委派要執行的對向(方法),必須含有一個 Task<TResult> 型別參數。
Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult> continuationFunction)
這個方法表示:
- ContinueWith 會回傳一個 Task<TNewResult> 型別值。
- ContinueWith 的參數是一個 Func 委派。
- 該委派要執行的對向(方法),必須含有一個 Task<TResult> 型別參數,也就是將已完成的工作當做引數傳遞給委派。
例:
private Photo UpdatePhoto2(Task<AtomEntry> task) { return new Photo(); } private void bnTaskResultContinueWith_Click(object sender, EventArgs e) { FileInfo[] files = GetFileList(); foreach (FileInfo file in files) { string filename = file.FullName; Task<Photo> task = Task.Factory .StartNew<AtomEntry>(UploadPhoto2, filename) .ContinueWith<Photo>(UpdatePhoto2); Photo photo = task.Result; Console.WriteLine("ID:{0} Title:{1} Summary:{2}", photo.Id, photo.Title, photo.Summary); } Console.WriteLine("Finish"); }
叫用 ContinueWith 時,你無法傳送額外的參數給執行方法,若須使用到參數,可考慮改用 Lambda 。
foreach (FileInfo file in files) { string filename = file.FullName; Task<Photo> task = Task.Factory .StartNew<AtomEntry>(UploadPhoto2, filename) .ContinueWith<Photo>(pre_task => { Photo ori_photo = new Photo(); ori_photo.AtomEntry = pre_task.Result; string title = Path.GetFileNameWithoutExtension(filename); string summary = title; return UpdatePhoto(ori_photo.Id, title, summary); }); Photo photo = task.Result; Console.WriteLine("ID:{0} Title:{1} Summary:{2}", photo.Id, photo.Title, photo.Summary); }
Parallel
Parallel 類別是專門用來支援平行迴圈的作業模式,也就是簡化上面範例中,使用 Loop + Task 的程式碼。 所以,若一個集合中的所有元素,都要使用相同的操作行為,就非常適合使用 Parallel 類別來進行同步處理。
private void UploadFile0(string filename) { UploadFileAsync(gsAlubmId, filename); } private void bnParallel_ForEach_Click(object sender, EventArgs e) { List<string> files = GetFileList(); // 建立 ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, Action<TSource> body); Parallel.ForEach<string>(files, delegate(string file) { UploadFile0(file); }); // 上一行程式碼,可以用 Lambda 簡化如下: Parallel.ForEach(files, (file) => { UploadFile0(file); }); // 如果要執行的程式碼都包在一個函式裡,且符合委派簽名,那麼還可以簡化如下: Parallel.ForEach(files, UploadFile0); }
注意事項:
- Parallel.ForEach 迴圈,並不保證每個並行工作的啟動先後。
- 每個並行工作都必須執行完畢,程式才會繼續往下執行迴圈外的程式。
沒有留言:
張貼留言