並行編程和任務(二)
- 2019 年 11 月 8 日
- 筆記
前言
上一篇我們主要介紹了並行編程相關的知識,這一節我們繼續介紹關於任務相關的知識。為了更好的控制並行操作,我們可以使用System.Threading.Tasks中的Task類。我們首先來了解是什麼是任務——任務表示將要完成的一個或某個工作單元,這個工作單元可以在單獨執行緒中運行,也可以使用同步方式啟動運行(需要等待主執行緒調用)。為什麼使用任務呢?——任務不僅可以獲得一個抽象層(將要完成的工作單元)、還可以對底層的執行緒運行進行更好更多的控制(任務的運行)。
使用執行緒池的任務
我們講到使用任務可以更好更多的控制底層的執行緒。就涉及到——執行緒池,執行緒池提供的是一個後台執行緒的池。執行緒池獨自管理執行緒、根據需求增加或減少執行緒數。使用完成的執行緒返回至執行緒池中。我們下面就看看創建任務:
我們看下創建任務的幾種方式:
1、使用實例化的TaskFactory類,然後使用其StartNew方法啟動任務。
2、使用Task靜態的Factory以來訪問TaskFactory,然後調用StartNew方法啟動任務。與第一種相似,但是對工廠的創建的控制就沒那麼全面。
3、使用Task的構造函數,實例化Task對象來指定創建任務,然後通過Start()方法進行啟動任務。
4、使用Task.Run方法來立即啟動任務。
我們看下以上方法創建的任務有何區別和相同吧,看程式碼:
private static object _lock = new object();
public static void TaskMethond(object item) { lock (_lock) { Console.WriteLine(item?.ToString()); Console.WriteLine($"任務Id:{Task.CurrentId?.ToString() ?? "沒有任務運行"}t 執行緒Id:{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"是否是執行緒池中的執行緒:{Thread.CurrentThread.IsThreadPoolThread}"); Console.WriteLine($"是否是後台執行緒:{Thread.CurrentThread.IsBackground}"); Console.WriteLine(); } } #region 任務創建 public static void TaskCreateRun() { var taskFactory = new TaskFactory(); var task1 = taskFactory.StartNew(TaskMethond, "使用實例化TaskFactory"); var task2 = Task.Factory.StartNew(TaskMethond, "使用Task靜態調用Factory"); var task3 = new Task(TaskMethond, "使用Task構造函數實例化"); task3.Start(); var task4 = Task.Run(() => TaskMethond("使用Task.Run")); } #endregion
我們看程式碼運行的結果,發現不管使用的那種方法創建任務,都是使用過的執行緒池中的執行緒。
使用單獨執行緒的任務
任務當然也不一定就是使用執行緒池中的執行緒運行的,也是可以使用其他執行緒的。如果任務的將長時間運行的話,我們儘可能的考慮使用單獨執行緒運行(TaskCreationOptions.LongRunning),這個情況下執行緒就不由執行緒池管理。我們看下不使用執行緒池中執行緒運行,使用單獨執行緒運行的任務。
#region 單獨執行緒運行任務 public static void OnlyThreadRun() { var task1 = new Task(TaskMethond, "單獨執行緒運行任務", TaskCreationOptions.LongRunning); task1.Start(); } #endregion
我們看其運行結果,依舊是後台執行緒,但是不是使用的執行緒池中的執行緒。
使用同步任務
同時任務也可以同步運行,以相同的執行緒作為調用執行緒,下面我們看下使用Task類中的RunSynchronoushly方法實現同步運行。
#region 同步任務 public static void TaskRunSynchronoushly() { TaskMethond("主執行緒調用"); var task1 = new Task(TaskMethond, "任務同步調用"); task1.RunSynchronously(); } #endregion
我們看運行結果,發現首先調用TaskMethond方法時候沒有任務並且使用的執行緒1,再我們創建Task實例運行TaskMethond方法的時候,任務id是1,但是執行緒我們依然使用的是主執行緒1。
連續任務
在任務中,我們可以指定在某個任務完成後,應該馬上開始另外一個任務。好比一個任務完成之後應該繼續其處理。但是失敗後我們應該進行一些處理工作。
我們可以使用ContinueWith()方法來定義使用連續任務,表示某任務之後應該開始其他任務,我們也可以指定任務成功後開始某個任務或者失敗後開啟某個任務(TaskContinuationOptions)。
#region 連續任務 public static void TaskOne() { Console.WriteLine($"任務{Task.CurrentId},方法名:{System.Reflection.MethodBase.GetCurrentMethod().Name }啟動"); Task.Delay(1000).Wait(); Console.WriteLine($"任務{Task.CurrentId},方法名:{System.Reflection.MethodBase.GetCurrentMethod().Name }結束"); } public static void TaskTwo(Task task) { Console.WriteLine($"任務{task.Id}以及結束了"); Console.WriteLine($"現在開始的 任務是任務{Task.CurrentId},方法名稱:{System.Reflection.MethodBase.GetCurrentMethod().Name } "); Console.WriteLine($"任務處理"); Task.Delay(1000).Wait(); } public static void TaskThree(Task task) { Console.WriteLine($"任務{task.Id}以及結束了"); Console.WriteLine($"現在開始的 任務是任務{Task.CurrentId}.方法名稱:{System.Reflection.MethodBase.GetCurrentMethod().Name } "); Console.WriteLine($"任務處理"); Task.Delay(1000).Wait(); } public static void ContinueTask() { Task task1 = new Task(TaskOne); Task task2 = task1.ContinueWith(TaskTwo, TaskContinuationOptions.OnlyOnRanToCompletion);//已完成情況下繼續任務 Task task3 = task1.ContinueWith(TaskThree, TaskContinuationOptions.OnlyOnFaulted);//出現未處理異常情況下繼續任務 task1.Start(); } #endregion
我們看程式碼中寫的是先開始運行TaskOne(),然後當任務完成後運行TaskTwo(Task task) ,如果任務失敗的話機會運行TaskThree(Task task)。我們看運行結果中是運行了TaskOne()然後成功後運行了TaskTwo(Task task),避開了TaskThree(Task task)的運行,所以我們是可以通過ContinueWith來進行連續任務和TaskContinuationOptions進行控制任務運行的。
任務層次—父子層次結構
這裡我們利用任務的連續性,我就就可以實現在一個任務結束後立即開啟另一個任務,任務也可以構成一個層次結構。就比如一個任務中啟動了一個任務,這樣的情況就形成了父子層次的結構。下面我們看的案例就是這麼一個案例。
#region 任務的層次結構——父子層次結構 public static void ChildTask() { Console.WriteLine("當前運行的子任務,開啟"); Task.Delay(5000).Wait(); Console.WriteLine("子任務運行結束"); } public static void ParentTask() { Console.WriteLine("父級任務開啟"); var child = new Task(ChildTask); child.Start(); Task.Delay(1000).Wait(); Console.WriteLine("父級任務結束"); } public static void ParentAndChildTask() { var parent = new Task(ParentTask); parent.Start(); Task.Delay(2000).Wait(); Console.WriteLine($"父級任務的狀態 :{parent.Status}"); Task.Delay(4000).Wait(); Console.WriteLine($"父級任務的狀態 :{parent.Status}"); } #endregion
等待任務
在前面問介紹的.Net非同步編程中我們講到了WhenAll,用於處理多個非同步方法。在這裡我們繼續擴展點,WhenAll()和WaitAll(),都是等待傳遞給他們的任務完成。但是WaitAll()方法阻塞調用任務,知道所有任務完成為止,而WhenAll()返回了一個任務,從而可以使用async關鍵在等待結果。不會阻塞任務。與之相對應的也還有WaitAny()和WhenAn()。等待任務還有我們一直都用到了的Task.Delay()方法,指定這個方法放回的任務前要等待的毫秒數。
下面我們看這個ValueTask等待類型(結構),相對於Task類來說,ValueTask沒有堆中對象的開銷。在一般情況下,Task類型的開銷可以被忽略掉,但是在一些特殊情況下,例如方法被調用千次萬次來看。這種情況ValueTask就變得很適用了。我們看下面這個案例,使用ValueTask時,在五秒內的情況下直接從它的構造函數返回值。如果時間不在五秒內的話就使用真正獲取數據的方法。然後我們與使用Task的方法進行對比。這裡我們採取十萬條數據的測試對比。
#region 等待任務 private static DateTime _time; private static List<string> _data; public static async ValueTask<List<string>> GetStringDicAsync() { if (_time >= DateTime.Now.AddSeconds(-5)) { return await new ValueTask<List<string>>(_data); } else { (_data, _time) = await GetData(); return _data; } } public static Task<(List<string> data, DateTime time)> GetData() => Task.FromResult( (Enumerable.Range(0, 10).Select(x => $"itemString{x}").ToList(), DateTime.Now)); public static async Task<List<string>> GetStringList() { (_data, _time) = await GetData(); return _data; } #endregion
static async Task Main(string[] args) { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Console.WriteLine("ValueTask開始"); for (int i = 0; i < 100000; i++) { var itemList= await GetStringDicAsync(); } Console.WriteLine("ValueTask結束"); Console.WriteLine($"ValueTask耗時:{stopwatch.ElapsedMilliseconds}"); Console.WriteLine(); Console.WriteLine(); stopwatch.Restart(); Console.WriteLine("Task開始"); for (int i = 0; i < 100000; i++) { var itemList = await GetStringList(); } Console.WriteLine("Task結束"); Console.WriteLine($"Task耗時:{stopwatch.ElapsedMilliseconds}"); Console.ReadLine(); }
我們看其運行結果,使用Task和ValueTask的運行結果耗時相差是巨大的。所以在一些特殊情況下使用ValueTask或許會更加的適用。
總結
今天我們介紹了關於任務相關的一些知識概念。我們結合上一篇文章我們來梳理一些任務、執行緒、多執行緒、非同步、同步、並發、並行任務之間的聯繫與關係吧。
首先我們看我們這章節學習的任務、任務是一個將要完成的工作單元,那麼由誰完成呢?由執行緒來運行這個任務。那麼關於多執行緒呢?多執行緒應該可以說是一個設計概念,用來實現執行緒切換的。多執行緒就可以運行多個任務,但是在並發中。在同一時間內只能有一個程式運行。只不過執行緒間切換速度極快,讓它看起來似乎是在同一時間運行了多個程式。其實在微觀上講,並發在任意時間點只有一個程式在運行,只不過是執行緒切換速度快。那麼怎麼達到切換速度極快呢?這就需要非同步了。在執行緒運行的時候不需要等到它完成結果再去繼續其他的執行緒任務。也就是可等待的。實現A運行起來不等待其結果,然後切換到B繼續運行。這樣切換速度就極快了。我們再仔細看多執行緒切換執行緒似乎成了實現非同步的一種方法手段了。有非同步就有同步,同步來說就不需要使用到多執行緒了,沒必要。反正等到上一個任務運行完成。就繼續使用上一個執行緒繼續運行。這裡都是講的並發中的情況。那麼並行呢?並行可以說不管在微觀還是宏觀上都是可以實現一個時間運行多個程式的。並發是多個程式運行在一個處理機上,但是並行任務是運行在多個處理機上。例如實現四個任務並行,那麼我們至少需要四個邏輯處理內核的配合才能到達。
世界上那些最容易的事情中,拖延時間最不費力。堅韌是成功的一大要素,只要在門上敲得夠久夠大聲,終會把人喚醒的。
歡迎大家掃描下方二維碼,和我一起學習更多的知識?