14.并发与异步 - 2.任务Task -《果壳中的c#》
線程是創建并發的底層工具,因此具有一定的局限性。
- 沒有簡單的方法可以從聯合(Join)線程得到“返回值”。因此必須創建一些共享域。當拋出一個異常時,捕捉和處理異常也是麻煩的。
- 線程完成之后,無法再次啟動該線程。相反,只能聯合(Join)它(在進程阻塞當前線程)。
與線程相比,Task是一個更高級的抽象概念,它標識一個通過或不通過線程實現的并發操作。
任務是可組合的——使用延續將它們串聯在一起。它們可以使用線程池減少啟動延遲,而且它們可以通過TaskCompletionSource使用回調方法,避免多個線程同時等待I/O密集操作。
14.3.1 啟動任務
從Framework 4.5開始,啟動一個由后臺線程實現的Task,最簡單的方法是使用靜態方法Task.Run。調用時需要傳入一個Action代理:
Task.Run(() => Console.WriteLine("hello"));Task.Run是Framework 4.5新引入的方法,在Framework 4.0中,調用Task.Factory.StartNew,可以實現相同效果,前者相當于后者的快捷方式。
Task默認使用線程池,它們都是后臺線程。意味當主線程結束時,所有任務都會隨之停止。因此,要在控制臺應用程序中運行這些例子,必須在啟動任務之后阻塞主線程。例如,掛起(Waiting)該讓你誤,或者調用Console.ReadLine:
static void Main(string[] args){Task.Run(() => Console.WriteLine("Foo"));Console.ReadLine();}采用這種方式調用Task.Run,與下面啟動線程方式類似(唯一不同的是沒有隱含使用線程池):
new Thread(() => Console.WriteLine("Foo")).Start();Task.Run會返回一個Task對象,它可以用來監控任務執行過程,這一點與Thread對象不同。(這里沒有調用Start,因為Task.Run創建是“熱”任務;相反,想創建“冷”任務,必須使用Task構造函數,但這種方法在實踐中很少用)
任務的Status屬性可用于跟蹤任務的執行狀態。
1.等待(Wait)
調用Wait方法,可以阻塞任務,直至任務完成,效果等同于Thread.Join:
Task task = Task.Run(() =>{Thread.Sleep(2000);Console.WriteLine("Foo");});Console.WriteLine(task.IsCompleted); //Falsetask.Wait();//阻塞,直至任務完成Console.WriteLine(task.IsCompleted); //TrueConsole.ReadLine();可以在Wait中指定一個超時時間和一個取消令牌。
2.長任務
默認情況下,CLR會運行在池化線程上,這種線程非常適合執行短計算密集作業。如果要執行長阻塞操作,則可以按下面方式避免使用池化線程:
Task task = Task.Factory.StartNew(() =>{Console.WriteLine("Task started");Thread.Sleep(2000);Console.WriteLine("Foo");}, TaskCreationOptions.LongRunning);task.Wait(); // Blocks until task is complete提示:
在池化線程上運行一個長任務問題并不大,但是如果要同時運行多個長任務(特別會阻塞的任務),則會對性能產生影響。在這種情況下,通常更好的方法是使用TaskCreationOptions.LongRunning:
- 如果運行I/O密集任務,則可以使用TaskCompletionSource和異步函數,通過回調函數(延續)實現并發性,而不通過線程實現。
- 如果是運行計算密集任務,則可以使用一個生產者/消費者隊列,控制這些任務的并發數量,避免出現線程和進程阻塞的問題。
14.3.2 返回值
Task<TResult>允許任務返回一個值。調用Task.Run,傳入一個Func<TResult>代理(或者兼容的Lambda表達式),代替Action,就可以獲得一個Task:
Task<int> task = Task.Run (() => { Console.WriteLine ("Foo"); return 3; });int result = task.Result; // Blocks if not already finished Console.WriteLine (result); // 3下面的例子創建一個任務,它使用LINQ就按前3百萬個整數(從2開始)中的素數個數:
Task<int> primeNumberTask = Task.Run(() =>Enumerable.Range(2, 3000000).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));Console.WriteLine("Task running...");Console.WriteLine("The answer is " + primeNumberTask.Result);這段代碼會打印“Task running...”,然后幾秒鐘后打印216815。
14.3.3 異常
與線程不同,Task可以隨時拋出異常。
任務代碼拋出一個未處理異常,那么這個異常會自動傳遞到調用Wait()的任務上或者訪問Task<TResult>的Result屬性的代碼上:
CLR會將異常封裝在AggregateException中,從而更適合并行編程場景;
使用Task的IsFaulted和IsCanceled屬性,就可以不重新拋出異常而檢測出錯的任務。
如果都返回false,則沒有出錯;
IsCanceled為true,任務拋出 OperationCanceledOPeration;
IsFaulted為true,則任務拋出另一種異常,而Exception屬性包含該錯誤。
1.異常和自主任務
使用靜態事件 TaskScheduler.UnobservedTaskException,可以在全局范圍訂閱為監控的異常;處理這個事件,然后記錄發生的錯誤,是一個很好的異常處理方法。
14.3.4 延續
延續(continuation)告訴任務在完成之后繼續執行下面的操作。
延續通常由一個回調方法實現,它會在操作完成之后執行一次。
給一個任務附加延續的方法有兩種:
第一種是C# 5.0異步功能使用的方法GetAwaiter方法
Task<int> primeNumberTask = Task.Run (() =>Enumerable.Range (2, 3000000).Count (n => Enumerable.Range (2, (int)Math.Sqrt(n)-1).All (i => n % i > 0)));//獲取用于等待此 System.Threading.Tasks.Task<TResult>的等待者 var awaiter = primeNumberTask.GetAwaiter(); //將操作設置為當 System.Runtime.CompilerServices.TaskAwaiter<TResult> 對象停止等待異步任務完成時執行 awaiter.OnCompleted (() => {int result = awaiter.GetResult(); //異步任務完成后關閉等待任務Console.WriteLine (result); //打印結果 });調用GetAwaiter會返回一個等待者(awaiter)對象,它的方法會讓先導(antecedent)任務(primeNumberTask)在完成(或出錯)之后執行一個代理已經完成的任務也可以附加一個延續,這時延續就馬上執行。
提示:
等待者可以是任意對象,但它必須包含前面所示兩個方法(OnCompleted和GetResult)和一個Boolean類型屬性IsCompleted對象,它不需要實現包含所有這些成員的特定接口或繼承特定基類。
調用GetResult()的好處在于,一旦先前的Task有異常,就會拋出該異常。而且該異常和之前演示的異常不同,它不需要經過AggregateException再包裝了。
另一種附加延續的方法是調用任務的ContinueWith方法:
Task<int> primeNumberTask = Task.Run (() =>Enumerable.Range (2, 3000000).Count (n => Enumerable.Range (2, (int)Math.Sqrt(n)-1).All (i => n % i > 0)));primeNumberTask.ContinueWith (antecedent => {int result = antecedent.Result;Console.WriteLine (result); // Writes 123 });ContinueWith本身返回一個Task,它非常適合添加更多延續。然而,任務出錯,我們必須直接處理AggregateException,然后編寫額外代碼,將延續編列到UI應用程序。而非UI上下文中,如果要讓延續運行在同一個線程上,則必須指定TaskContinuationOptions.ExcuteSynchronously;否則彈回線程池。
14.3.5 TaskCompletionSource
前面介紹Task.Run如何創建一個在池化(或非池化)線程運行代理的任務。另一種就是TaskCompletionSource。
TaskCompletionSource可以創建任務,不包含任何必須在后面啟動和結束的操作。原理是提供一個可以手工操作的“附屬”任務——和其他任務一樣。然而,這個任務完全通過下面的方法由TaskCompletionSource對象控制:
public class TaskCompletionSource<TResult> {public void SetCanceled();public void SetResult(TResult result);public void SetException(Exception exception);public bool TrySetCanceled();public bool TrySetException(Exception exception);... }調用這些方法可以給任務發送信號,將任務修改為完成、異?;蛉∠麪顟B。
這些方法只能調用一次,如果多次調用SetCanceled、SetResult或SetException,將拋出異常,而Try***等方法則會返回false。
使用TaskCompletionSource,可以編寫自定義的Run方法:
static void Main(string[] args){Task<int> task = Run(() => { Thread.Sleep(5000); return 42; });Console.WriteLine(task.Result);Console.Read();}static Task<TResult> Run<TResult>(Func<TResult> function){var tcs = new TaskCompletionSource<TResult>();new Thread(() =>{try { tcs.SetResult(function()); }catch (Exception ex) { tcs.SetException(ex); }}).Start();return tcs.Task;}調用這個方法等同于使用TaskCreationOptions.LongRunning選項調用Task.Factory.StartNew,請求一個非池化線程。
TaskCompletionSource真正作用是創建一個不綁定線程的任務。例如,假設一個任務需要等待5秒鐘,然后返回數字42.我們可以使用Timer類實現,而不需要使用線程,由CLR在x毫秒之后觸發一個事件:
static void Main(string[] args){var awaiter = GetAnswerToLife().GetAwaiter();awaiter.OnCompleted(() => Console.WriteLine(awaiter.GetResult()));}static Task<int> GetAnswerToLife(){var tcs = new TaskCompletionSource<int>();// Create a timer that fires once in 5000 ms:var timer = new System.Timers.Timer(5000) { AutoReset = false };timer.Elapsed += delegate { timer.Dispose(); tcs.SetResult(42); };timer.Start();return tcs.Task;}通過給任務附加一個延續,就可以在不阻塞任何線程的前提下打印這個結果。
var awaiter = GetAnswerToLife().GetAwaiter();awaiter.OnCompleted(() => Console.WriteLine(awaiter.GetResult()));將延遲時間參數化,并且刪除返回值,可以優化這段代碼。并且將它變成一個通用的Delay方法。意味讓它返回一個Task而不是Task<int>。然而,TaskCompletionSource沒有泛型版本,因此無法創建一個非泛型任務。但變通方法很簡單:因為Task<TResult>派生自Task,所以創建一個TaskCompletionSource<anything>,然后將它隱式轉換為Task<anything>,就可以得到一個Task:
var tcs = new TaskCompletionSource<object>(); Task task = tcs.Task;寫出Delay方法,然后讓它5秒打印“42”:
static void Main(string[] args){Delay(5000).GetAwaiter().OnCompleted(() => Console.WriteLine(42));Console.Read();}static Task Delay(int milliseconds){var tcs = new TaskCompletionSource<object>();var timer = new System.Timers.Timer(milliseconds) { AutoReset = false };timer.Elapsed += delegate { timer.Dispose(); tcs.SetResult(null); };timer.Start();return tcs.Task;}不在線程上使用TaskCompletionSource,意味著只有在延續啟動時才創建線程。同時啟動10000個這種操作,而不會出錯或超出資源限制:
for (int i = 0; i < 10000; i++)Delay (5000).GetAwaiter().OnCompleted (() => Console.WriteLine (42));14.3.6 Task.Delay
Task.Delay是Thread.Sleep的異步版本
Task.Delay(5000).GetAwaiter().OnCompleted(()=>Console.WriteLine(42));或者
Task.Delay(5000).ContinueWith(ant => Console.WriteLine(42));轉載于:https://www.cnblogs.com/tangge/p/7231673.html
總結
以上是生活随笔為你收集整理的14.并发与异步 - 2.任务Task -《果壳中的c#》的全部內容,希望文章能夠幫你解決所遇到的問題。