異步線程 附屬篇
續接上片,做的進一步分析,本篇如果語述上有問題的,可以忽略
Thread部分
public class TestThread { public void RunThread() { //線程 聲明線程時傳入方法A,A可以帶object類型的參數,沒有返回值, //如果有參數,start啟用的時候傳入。默認是前臺運行的,就是說在所有的線程走完後才運行結束 Thread t1 = new Thread(() => { Thread.Sleep(2000); Console.WriteLine(222); }); Thread t2 = new Thread((object m) => { Thread.Sleep(6000); Console.WriteLine(m); }); t2.IsBackground = true; t2.Start(333); t1.Start(); Thread.Sleep(1000); Console.WriteLine(111); } ######## 測試部分 ######## TestThread tw = new TestThread(); tw.RunThread(); 結果: 111 222 CLR自動關閉應用程序 333出不來 分析: Thread 默認是前臺運行,前臺運行完 CLR自動結束(有的不是,比如說winForm是一直運行著的,如果控制臺,比如Console.Read(),保持前臺在等待著,也是OK的) t1 前臺運行 t2後臺運行,他們和主線程M一起並行運行 M t2 t1 停1s 停2s 停6s 輸出111 輸出222 --------------------------CLR結束 輸出333 ########################## //線程池初始化執行方法必須帶一個object參數 //ThreadPool默認帶一個object的參, 沒有返回值,默認是後臺運行的,就是說在只要主線程走完了,直接就完了,不管子線程走完沒 public void RunThreadPool() { ThreadPool.QueueUserWorkItem((object o) => { Thread.Sleep(6000); Console.WriteLine(222); }); Thread.Sleep(1000); Console.WriteLine(111); } ######## 測試部分 ######## TestThread tw = new TestThread(); tw.RunThreadPool(); 結果: 111 CLR自動關閉應用程序 222出不來 分析: ThreadPool 默認是後臺運行 ########################## public void RunParaller() { //Parallel是用多個線程執行循環的工具 //Parallel是個普通順序執行語句,只是裏面開啟多個線程跑東西,Parallerl下面的語句還是需要等Parallel執行完後才能執行的 int result = 0; int lockResult = 0; object lb = new object(); Parallel.For(0, 3, (i) => { result = result + 2; //lock只能lock引用類型,利用引用對象的地址唯一作為鎖,實現lock中的代碼一次只能一個線程訪問 //lock讓lock裏的代碼在並行時變為串行,盡量不要在parallel中用lock(lock內的操作耗時小,lock外操作耗時大時,並行還是起作用) lock (lb) { lockResult = lockResult + 2; Thread.Sleep(2000); Console.WriteLine("i={0},lockResult={1}", i, lockResult); } Console.WriteLine("i={0},result={1}", i, result); }); Console.WriteLine(11111); } TestThread tw = new TestThread(); tw.RunParaller(); ######## 測試部分 ######## 結果: i=0,lockResult=2 i=0,result=6 i=1,lockResult=4 i=1,result=6 i=2,lockResult=6 i=2,result=6 11111 分析: 這個栗子有點特殊啊,如果循環大了的話,result的值不知道會不會存在問題 ########################## }
可以看出,Thread多線程,傳入的是沒有返回值的方法,不涉及到各個新開的線程返回值的討論,討論的是誰先執行完誰後執行完的影響最後結構的問題,討論域是站在一個方法下,這個方法新開啟線程 討論的,接下來我們要切換到定位多線程執行返回值這塊
Task
Task是個什麽東西呢,它在線程中的定位是什麽,為啥會誕生它,先看下面的例子
public void TaskApply() { Console.WriteLine(1111); var t= Task.Run(() => { Thread.Sleep(8000); Console.WriteLine(5555); }); Console.WriteLine(2222); Thread.Sleep(1000); Console.WriteLine(3333); } ######## 測試部分 ######## TestTask tw = new TestTask(); tw.TaskApply(); Console.WriteLine(4444); 結果: 1111 2222 (停1s) 3333 4444 ----CLR結束 5555和6666不會出來 分析: 可以看出Task也是後臺運行的線程, 首先遇到Task ,相當於新運行了一個IsBackground為true的線程,Task.Run裏面也走,外面的也走,並行向下執行,外面的2222輸出後,停個1s,輸出3333,外面的走完了,繼續走方法外的,輸出4444,方法外的也走完了,這個時候TaskRun還在那Sleep,不管,結果就出來了 ########################## 從上面看,好像他和IsBackground=true的Thread運行特征好像相似點很高,我們捋一下,首先Thread是有前後臺運行的,他傳的方法可以帶一個object類型的參數,也可以不帶,沒有返回值,二ThreadPool是必須傳一個object的參數的,是後臺運行的,也沒有返回值。 但是呢,Task和他們還有有些不填點,Task他傳的方法不能帶參數,他是後臺運行的,重點是可以返回值的,我們可以在想調動它返回值的地方去獲取,唯一需要註意的是在獲取的時候會阻塞到當前的線程,即Task中的方法和調動他的變成串執行了,當然調用值的時候,可能Task中已經執行完了也不好說。看下面的改動 public void TaskApply2() { Console.WriteLine(222); var t= Task.Run(() => { Thread.Sleep(8000); Console.WriteLine(555); return 666; }); Console.WriteLine(333); Thread.Sleep(1000); Console.WriteLine(444); Console.WriteLine(t.Result); Console.WriteLine(777); } ######## 測試部分 ######## Console.WriteLine(111); TestTask tw = new TestTask(); tw.TaskApply(); Console.WriteLine(888); 結果: 111 222 333 444 555 666 777 888 分析: 首先最外面輸出 111,然後進入方法B裏面,輸出222,然後遇到 Task,並行開始了,外面(B)輸出333,停個1s,輸出444,遇到t.Result,不好意思,得等待Task運行完才能向下執行了,這時候Task.Run裏面的8s到了輸出555,把返回值返出來,外面(B)的輸出666,接著輸出777,最後最外面輸出888 可以看出 如果不調返回值的話,外面(B)是是不會阻塞著等待Task.Run裏面的執行完再執行的,這就是Task返回值阻塞的特性
當明白上面這個栗子和分析之後,我們看下面幾個簡單的就很通順了
public void RunBackTask() { Task t = new Task(() => { Thread.Sleep(6000); Console.WriteLine(333); }); t.Start(); Console.WriteLine(111); Thread.Sleep(2000); Console.WriteLine(222); } //外面調用 RunBackTask這個方法的時候 首先是輸出 111,停個2s,輸出 222,Task的6s還沒到,這時候外面的繼續跑,這個333輸出與否就要看外面的了 而我們要看下面只個鬼的話 public void RunTaskGui() { // 111 222 333 444 555 666 順序執行 var ff = Task.Run(() => { Thread.Sleep(6000); Console.WriteLine(111); return 333; }).Result; Console.WriteLine(222); Console.WriteLine(ff); Thread.Sleep(6000); Console.WriteLine(444); } 外面的調RunTaskGui(),裏面的順序的走完 依此是 111 222 333 444,因為 Task.Run()後直接調了Result,直接阻塞了,這個方法和普通的方法就一樣了
從上面我們發現,我們一直在討論調用方法A裏面是怎麽執行的,外面的方法肯定是在調用方法A直線完後再直線,即使調用方法A裏面有Task的一個異步線程B,那也是新開的一個並行的線程,雖然這個線程B不阻塞他下面的語句和我們外部的方法,我們也是在調用方法B裏的走到最後的 } 後再直線我們外面下面的方法
我們現在要切換到,管你調動方法A裏面執行什麽東東,我外面調用你後,我直接往下面走,看似外面又一層異步的調用
public int SubTask()
{
Thread.Sleep(1000);
Console.WriteLine(333);
var ff = Task.Run(() =>
{
Thread.Sleep(6000);
Console.WriteLine(555);
return 666;
});
Console.WriteLine(444);
Console.WriteLine(ff.Result);
return 777;
}
######## 測試部分 ########
Console.WriteLine(111);
var s=Task.Run(() =>
{
TestTask tm = new TestTask();
return tm.SubTask();
});
Console.WriteLine(222);
Console.WriteLine(s.Result);
Console.WriteLine(888);
結果:
111
222
333
444
555
666
777
888
這個我就不解釋了,應該能順下來
接下來看另一個鬼 async + await
async+await
public async void RunAwaitTask()
{
Thread.Sleep(1000);
Console.WriteLine(222);
var ff= await Task.Run(() =>
{
Thread.Sleep(2000);
Console.WriteLine(444);
return 666;
});
Console.WriteLine(555);
Console.WriteLine(ff);
Console.WriteLine(777);
}
######## 測試部分 ########
TestTask tm = new TestTask();
Console.WriteLine(111);
tm.RunAwaitTask();
Console.WriteLine(333);
//Thread.Sleep(6000);
//Console.WriteLine(888);
結果:
111
222
333 --走完後 ClR可能會結束,所以下面不一定會出來
444
555
666
777
把
//Thread.Sleep(6000);
//Console.WriteLine(888);
放開後
結果是:
111
222
333
444
555
666
777
888
它和上面我們討論的哪個最相似,找找,我們就發現了這貨,比較下
public void TaskApply2()
{
Console.WriteLine(222);
var t= Task.Run(() =>
{
Thread.Sleep(8000);
Console.WriteLine(555);
return 666;
});
Console.WriteLine(333);
Thread.Sleep(1000);
Console.WriteLine(444);
Console.WriteLine(t.Result);
Console.WriteLine(777);
}
######## 測試部分 ########
TestTask tw = new TestTask();
Console.WriteLine(111);
tw.TaskApply();
Console.WriteLine(888);
結果:
111
222
333
444
555
666
777
888
看到異同點了沒,看到了沒,看到了沒,看到了沒,沒看到說明上面Task的思想沒定下來
我們發現調用 一個async的方法,在進去裏面一步步執行的時候,遇到await,開始並行了,這個並行現在變成了最外層的調用方法了,而不是裏面的語句
從例子看,在執行完Console.WriteLine(222);後,RunAwaitTask這個遇到了await就開始等待了,await下面的語句就阻塞,而最外層的Console.WriteLine(333);可以走了,也就是說,await 的Task運行的和最外層的一起走並行
對於TaskApply2呢,在遇到 Task後,Task下面的語句Console.WriteLine(333);開始執行了,並行的是下面的語句,而不是外面 Console.WriteLine(888);對於Console.WriteLine(888);只是在TaskApply2方法走完 } 後才走,這裏面如果把 Console.WriteLine(t.Result);去掉的話,結果又會不一樣,但中心區別已經找到了:
async+await 這個 開始並發的點是在遇到await後,外層調用它的和他的Task並行,外層的語句就開始執行了。而普通的Task不會和外層的調用有關系,只會和Task下面的語句開啟並行,外層的是在走完 } 後才執行的。
上面只是剛引入 async+await的東西,他們還有各種限制和特征,比如:
異步方法返回類型是Task<T>,Task,或者Void,所以獲取返回值只能await或者.Result,但是 async+await和async+.Result還是不一樣的
public async void RunAwaitTask()
{
Thread.Sleep(1000);
Console.WriteLine(222);
var ff=Task.Run(() =>
{
Thread.Sleep(2000);
Console.WriteLine(444);
return 666;
}).Result;
Console.WriteLine(555);
Console.WriteLine(ff);
Console.WriteLine(777);
}
######## 測試部分 ########
TestTask tm = new TestTask();
Console.WriteLine(111);
tm.RunAwaitTask();
Console.WriteLine(333);
結果:
111
222
444
555
666
777
333
可以看出關鍵點在於await
不解釋了
這只是最簡單開始,還會有各種異步嵌套。但這個最基本的區別腦子裏必須定下來,這樣遇到各個的場景就能想版本應用和實現了,也能斷定自己寫的代碼該怎麽走了,。線程牽扯到各個線程按不同的時間段一起執行,誰先執行完,誰後執行完都會造成不同的結果。所有有了指導思想就可以慢慢診斷了
異步線程 附屬篇