接觸Abp之后,發(fā)現(xiàn)有很多我不熟悉的,原來在.net開發(fā)中沒用到的知識點(diǎn),比如線程相關(guān)的Task ,async ,await等。說實(shí)話是真的不了解,就是知道是異步執(zhí)行,Abp框架中很多這樣用的,就模仿著去用了,直到項(xiàng)目上線后經(jīng)常出現(xiàn)一些莫名奇妙的錯(cuò)誤。如下面的錯(cuò)誤:
System.NotSupportedException: A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.
所以特意抽出點(diǎn)時(shí)間把這個(gè)知識點(diǎn)學(xué)習(xí)一下。
這里附上一個(gè)面試場景:
面試官:說說你對JavaScript閉包的理解吧?
我:嗯,平時(shí)都是前端工程師在寫JS,我們一般只管寫后端代碼。
面試官:你是后端程序員啊,好吧,那問問你多線程編程的問題吧。
我:一般沒用到多線程。
面試官:............................. (面試結(jié)束)
聽起來雖然像是個(gè)笑話,估計(jì)我去面試和這個(gè)差不多吧,哈哈,廢話不說了,趕緊補(bǔ)起來。
本篇目錄
準(zhǔn)備工作
線程時(shí)光之旅
static void Main(string[] args)
{
#region 創(chuàng)建線程
Console.WriteLine("我是主線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
//創(chuàng)建Thread線程-寫法1
var thread1 = new Thread(new ThreadStart(CreateGo));//Console # 1.0
//啟動線程
thread1.Start();
//創(chuàng)建Thread線程-寫法2
var thread2 = new Thread(new ThreadStart(CreateGo));//Console # 1.0
//啟動線程
thread2.Start();
//創(chuàng)建Thread線程-寫法3
var thread3 = new Thread(() => { new ThreadStart(CreateGo)(); });//new Thread(() => { CreateGo(); })
//啟動線程
thread3.Start();
//上面的縮寫形式 new Thread(CreateGo).Start();//Console # 1.0
Task.Factory.StartNew(CreateGo);//4.0
Task.Run(new Action(CreateGo));//4.5
Console.WriteLine("主線程結(jié)束:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
#endregion
}
public static void CreateGo()
{
Console.WriteLine("我是線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
}
需要注意在創(chuàng)建Thread實(shí)例之后,需要手動調(diào)用 Start()方法將其啟動。但對于Task來說,StartNew和Run,既會創(chuàng)建新的線程,同時(shí)還是立即啟動它。
運(yùn)行代碼:
static void Main(string[] args)
{
#region Thread
Console.WriteLine("我是主線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
var thread = new Thread(new ThreadStart(CreateThreadGo);
thread.Start();
Console.WriteLine("主線程結(jié)束");
#endregion
}
public static void CreateThreadGo()
{
Console.WriteLine("我是子線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
}
運(yùn)行結(jié)果:

F12 查看ThreadStart ,是一個(gè)無參數(shù)無返回值的委托,如果要在線程中執(zhí)行一個(gè)有參數(shù)的方法要如何作業(yè)呢?
namespace System.Threading
{
// 摘要:
// 表示在 System.Threading.Thread 上執(zhí)行的方法。
[ComVisible(true)]
public delegate void ThreadStart();
}
F12 查看Thread方法,可以看到Thread有四個(gè)構(gòu)造函數(shù)如下:
[SecuritySafeCritical]
public Thread(ParameterizedThreadStart start);
[SecuritySafeCritical]
public Thread(ThreadStart start);
[SecuritySafeCritical]
public Thread(ParameterizedThreadStart start, int maxStackSize);
[SecuritySafeCritical]
public Thread(ThreadStart start, int maxStackSize);
F12進(jìn)入ParameterizedThreadStart 里面是可以帶有一個(gè)object參數(shù)的
[ComVisible(false)]
public delegate void ParameterizedThreadStart(object obj);
改造代碼如下:
static void Main(string[] args)
{
#region ThreadPara
Console.WriteLine("我是主線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
//沒有匿名委託之前,只能傳遞一個(gè)Object參數(shù),寫法如下:
var threadPara = new Thread(new ParameterizedThreadStart(CreateThreadParaGo));
threadPara.Start("Oject參數(shù)哦");
//有匿名委託之后,參數(shù)隨意嘍,寫法如下:
new Thread(delegate()
{
CreateThreadParaMoreGo("參數(shù)1", "參數(shù)2", "參數(shù)3");
}).Start();
//lambda表達(dá)式格式:
new Thread(() =>
{
CreateThreadParaMoreGo("arg1", "arg2", "arg3");
}).Start();
Console.WriteLine("主線程結(jié)束");
#endregion
}
public static void CreateThreadParaGo(object para)
{
Thread.Sleep(100);
Console.WriteLine("我是傳遞來的參數(shù)---------{0}",para);
Console.WriteLine("我是接收一個(gè)object參數(shù)的子線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
}
public static void CreateThreadParaMoreGo(string arg1,string arg2,string arg3)
{
Thread.Sleep(100);
Console.WriteLine("我是傳遞來的參數(shù)----------{0},{1},{2}", arg1, arg2, arg3);
Console.WriteLine("我是接收多個(gè)參數(shù)的子線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
}
執(zhí)行結(jié)果:

上面的執(zhí)行結(jié)果中,子線程因?yàn)門hread.Sleep(100) ,所以每次都最后才打印出輸出結(jié)果,那么你可能會疑問,如果我想等子線程執(zhí)行完,我再執(zhí)行主線程后面的代碼,怎么辦?
如截圖變更代碼如下,運(yùn)行后結(jié)果:

static void Main(string[] args)
{
try
{
new Thread(CreateExGo).Start();
}
catch(Exception ex)
{
Console.WriteLine("Exception");
}
}
運(yùn)行后,不會執(zhí)行Catch里面的內(nèi)容,而是直接在程序中提示報(bào)錯(cuò)。由此可知在Thread中是沒辦法捕捉子線程異常。
試想一下,如果有大量的任務(wù)需要處理,例如網(wǎng)站后臺對于HTTP請求的處理,那是不是要對每一個(gè)請求創(chuàng)建一個(gè)后臺線程呢?顯然不合適,這會占用大量內(nèi)存,如果你的代碼設(shè)計(jì)了大量使用Thread,那么有可能會超過系統(tǒng)最大的線程數(shù)導(dǎo)致崩潰,而且每次創(chuàng)建和銷毀線程也是很耗資源,那怎么辦呢?線程池就是為了解決這一問題,把創(chuàng)建的線程存起來,形成一個(gè)線程池(里面有多個(gè)線程),當(dāng)要處理任務(wù)時(shí),若線程池中有空閑線程(前一個(gè)任務(wù)執(zhí)行完成后,線程不會被回收,會被設(shè)置為空閑狀態(tài)),則直接調(diào)用線程池中的線程執(zhí)行(例asp.net處理機(jī)制中的Application對象),
運(yùn)行代碼
#region ThreadPool
static void Main(string[] args)
{
for(int i=0;i<10;i )
{
ThreadPool.QueueUserWorkItem(x => { CreateThreadParaGo(i); });
}
}
#endregion
執(zhí)行結(jié)果:

由上面結(jié)果可以看出雖然執(zhí)行了10次,但并沒有創(chuàng)建10個(gè)線程而是創(chuàng)建了4個(gè)。
修改代碼如下:
#region ThreadPool
static void Main(string[] args)
{
for (int i = 0; i < 10; i )
{
ThreadPool.QueueUserWorkItem(new WaitCallback(CreateThreadParaGo),i);
}
}
#endregion
執(zhí)行結(jié)果:

先看一下waitback的定義:

一個(gè)帶參數(shù)的委托,這就要求它的委托方法必須帶一個(gè)object的參數(shù)了
ThreadPool靜態(tài)類通過QueueUserWorkItem()方法將工作函數(shù)排入線程池,它不需要我們主動的.Start(),那么他能不能Join()了? 下圖是QueueUserWorkItem返回對象,發(fā)現(xiàn)沒有Start和Join這樣的方法。
在上面有看了Thread構(gòu)造函數(shù),以及ThreadPool應(yīng)用,發(fā)現(xiàn)都是沒有返回值的委托,如果我們要想在主線程中獲取子線程執(zhí)行方法的返回值,該如何作業(yè)? Task隆重登場。
Task是.NET4.0加入的,跟線程池ThreadPool的功能類似,用Task開啟新任務(wù)時(shí),會從線程池中調(diào)用線程,而Thread每次實(shí)例化都會創(chuàng)建一個(gè)新的線程。
運(yùn)行代碼
static void Main(string[] args)
{
#region Task
//new Task 創(chuàng)建方式-不帶參數(shù) ,需要手動啟動
Task task = new Task(CreateGo);
task.Start();
//new Task lambda表達(dá)式創(chuàng)建待參數(shù)的線程,參數(shù)可以隨意哦
Task task1 = new Task(() => CreateThreadParaGo("我是newTask 的lambda傳入的參數(shù)哦"));
task1.Start();
//Task.Factory創(chuàng)建-不帶參數(shù),不用手動Start
Task task2 = Task.Factory.StartNew(CreateThreadGo);
//Task.Factory創(chuàng)建-帶參數(shù),不用手動Start
Task task3 = Task.Factory.StartNew(()=>CreateThreadParaMoreGo("factory lambda表達(dá)式參數(shù)1","factory lambda表達(dá)式參數(shù)2","factory lambda表達(dá)式參數(shù)3"));
//Task.Run 創(chuàng)建線程,不用手動Start
Task task4=Task.Run(()=>CreateGo());
Task task5 = Task.Run(() => CreateThreadParaGo("我是TaskRun"));
#endregion
}
運(yùn)行結(jié)果:

由上面代碼可知,Task有三種創(chuàng)建線程的方式,除了Task構(gòu)造函數(shù)需要手動啟動外,其他兩種無需調(diào)用Start手動啟動且開啟的是后臺線程。
上面執(zhí)行結(jié)果可知task在主線程先結(jié)束,其他子線程各自運(yùn)行。如果要在主線程執(zhí)行之前等待子線程結(jié)束,可以使用Wait()方法,在子線程結(jié)束后繼續(xù)主線程。
在上述代碼中加入task1.Wait() 就可以讓主線程等待task1結(jié)束后再繼續(xù)執(zhí)行。
運(yùn)行代碼:
static void Main(string[] args)
{
#region Task 返回值
Task<string> task = Task.Run(() => GetDayOfThisWeek());
var result = task.Result;
Console.WriteLine("今天是:{0}", result);
var dayName = Task.Run<string>(() => { return GetDayOfThisWeek(); });
Console.WriteLine("今天是:{0}",dayName.Result);
#endregion
}
public static string GetDayOfThisWeek()
{
Thread.Sleep(1000);
return DateTime.Now.DayOfWeek.ToString();
}
運(yùn)行結(jié)果:

大家可能注意到通過task.Result獲取子線程的返回值,主線程是在等子線程執(zhí)行完之后才打印并輸出的。task.Result除了拿到返回值外,和Wait()類似。
前不久主管還特意讓我看了關(guān)于javascript的promise。就和ContinueWith很像。
運(yùn)行代碼:
#region
static void Main(string[] args)
{
Task.Run(() => { CreateGo(); })
.ContinueWith(x => { CreateThreadParaMoreGo("接續(xù)任務(wù)21", "接續(xù)任務(wù)22", "接續(xù)任務(wù)23"); })
.ContinueWith(x => { CreateThreadParaGo("接續(xù)任務(wù)1"); });
#endregion
}
運(yùn)行結(jié)果:

執(zhí)行順序是有先后的,總是按照接續(xù)的任務(wù)順序進(jìn)行執(zhí)行。不過執(zhí)行線程的Id有可能是相同的,因?yàn)門ask是從線程池中啟動線程。
在 ContitueWith 的代碼中執(zhí)行結(jié)果中可以看出,主線程提前結(jié)束了。如果要想等到所有線程都結(jié)束在結(jié)束主線程,如何修改? 修改代碼如下:
- 如果要所有任務(wù)都完成,才完成主線程可以用Waitall();
static void Main(string[] args)
{
#region WaitAll
var task1=Task.Run(()=>CreateGo());
var task2=Task.Run(()=> CreateThreadParaGo("接續(xù)任務(wù)1"));
var task3=Task.Run(()=>CreateThreadParaMoreGo("接續(xù)任務(wù)21", "接續(xù)任務(wù)22", "接續(xù)任務(wù)23"));
Task.WaitAll(task1,task2,task3);
#endregion
}
運(yùn)行結(jié)果:

- 等任意一個(gè)任務(wù)結(jié)束就繼續(xù)主線程用WaitAny和WhenAny
static void Main(string[] args)
{
#region WaitAll
var task1=Task.Run(()=>CreateGo());
var task2=Task.Run(()=> CreateThreadParaGo("接續(xù)任務(wù)1"));
var task3=Task.Run(()=>CreateThreadParaMoreGo("接續(xù)任務(wù)21", "接續(xù)任務(wù)22", "接續(xù)任務(wù)23"));
Task.WaitAny(task1,task2,task3);
#endregion
}
運(yùn)行結(jié)果:

運(yùn)行代碼:
static void Main(string[] args)
{
try
{
var task = Task.Run(() => { CreateExGo(); });
task.Wait();
var task2 = Task.Run<string>(() => { return CreateExGoString(); });
var name = task2.Result;
}
catch (Exception ex)
{
Console.WriteLine("Exception:" ex.Message);
}
}
public static void CreateExGo()
{
throw null;
}
public static string CreateExGoString()
{
throw new Exception("TEST");
}
下面分若干種情況討論一下這個(gè)異常結(jié)果:
- 運(yùn)行上述代碼,能夠拋轉(zhuǎn)出異常,不會執(zhí)行task2
- 將task.Wait()及后面代碼全部屏蔽后運(yùn)行,不會輸出異常信息,但在代碼中提示異常,此處和Thread異常還不同,Thread 跑出異常后會一直運(yùn)行異常,無法正常跳出程序,而Task只會拋出一次異常。
- 將第一個(gè)task屏蔽后,運(yùn)行task2 能捕捉并提示異常,但是并不能得到task中的自定義提示異常信息。
由上面的分析可知,第一種無返回值的task需要手動調(diào)用Wait()方法,以使主線程中能夠捕捉子線程異常。而又返回至的task2則不需要再次調(diào)用,用taskResult除了返回值還類似Wait()功能。 主線程無法捕捉子線程的具體異常信息,尤其是自定義異常信息。
async用來修飾方法,表明這個(gè)方法是異步的,聲明的方法的返回類型必須為:void,Task或Task。
await必須用來修飾Task或Task,一般出現(xiàn)在已經(jīng)用async關(guān)鍵字修飾的異步方法中。通常情況下,async/await成對出現(xiàn)才有意義,
先用一段代碼看一下async和await的工作方式:
static void Main(string[] args)
{
#region async
Console.WriteLine("當(dāng)前是主線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
Task<int> task = GetStrLengthAsync();
Console.WriteLine("主線程繼續(xù)執(zhí)行");
Console.WriteLine("Task返回的值" task.Result);
#endregion
}
static async Task<int> GetStrLengthAsync()
{
Console.WriteLine("GetStrLengthAsync方法開始執(zhí)行:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
//此處返回的<string>中的字符串類型,而不是Task<string>
string str = await GetString();
Console.WriteLine("GetStrLengthAsync方法執(zhí)行結(jié)束");
return str.Length;
}
static Task<string> GetString()
{
Console.WriteLine("GetString方法開始執(zhí)行:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
return Task<string>.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Task.run:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
return "GetString的返回值";
});
}
運(yùn)行結(jié)果:

執(zhí)行順序如下:
- main方法開啟一個(gè)線程開始運(yùn)行程序,輸出
當(dāng)前是主線程:Thread Id is 8
- 執(zhí)行GetStrLengthAsync ,輸出
GetStrLengthAsync方法開始執(zhí)行:Thread Id 8 。此處可知GetStrLengthAsync 方法中并沒有創(chuàng)建新的線程,還是主線程
- 遇到await 方法,此時(shí)并沒有執(zhí)行GetStrLengthAsync 后面的方法也沒有馬上返回到main函數(shù)中,而是執(zhí)行到了GetString 的第一行。 輸出
GetString方法開始執(zhí)行:Thread Id 8 ,由此可以判斷await這里并沒有開啟新的線程去執(zhí)行GetString方法,而是以同步的方式執(zhí)行了GetString方法。
- 執(zhí)行到GetString中的Task.Run()的時(shí)候才開啟了新的后臺線程,此時(shí)主線程開始繼續(xù)執(zhí)行,輸出
主線程繼續(xù)執(zhí)行 。
- 因?yàn)樵贕etStrLengthAsync中調(diào)用了await方法,在await方法沒有返回結(jié)果之前不會繼續(xù)執(zhí)行該方法里面的內(nèi)容。但是await不會阻塞主線程,所以才會有4的輸出,當(dāng)await方法結(jié)束后,會繼續(xù)執(zhí)行GetStrLengthAsync中其他方法,此時(shí)輸出
GetStrLengthAsync方法執(zhí)行結(jié)束
- 最后輸出
Task返回的值13 ,這時(shí)在await方法已經(jīng)完畢,直接可以取回結(jié)果輸出。
await不會開啟新的線程,當(dāng)前線程會一直運(yùn)行知道遇到能開啟新線程的異步方法,比如Task.Delay() 、Task.Run、Task.Factory.StartNew 去開啟線程,如果不適用.net提供的Async方法,就需要自己創(chuàng)建Task,創(chuàng)建一個(gè)新線程
上述代碼中已經(jīng)看到在async的方法前面使用await進(jìn)行修飾,可以讓主線程等待后臺線程執(zhí)行完畢,與wait類似,不過await不會阻塞主線程,只會讓上面的GetStrLengthAsync暫停運(yùn)行,等待await結(jié)果。
運(yùn)行代碼:
static void Main(string[] args)
{
Console.WriteLine("當(dāng)前是主線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
Test();
Console.WriteLine("主線程繼續(xù)執(zhí)行:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
}
static async Task Test()
{
Console.WriteLine("Test方法開始執(zhí)行:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
Task<string> task = Task.Run(() =>
{
Console.WriteLine("Test中Task方法開始執(zhí)行:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
return "Hello World";
});
Console.WriteLine("Test中task之後內(nèi)容開始執(zhí)行:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
string str = await task; //5 秒之后才會執(zhí)行這里
Console.WriteLine("await之後的方法:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(str);
}
運(yùn)行結(jié)果:

await通常情況下是放在用async修飾的方法中使用,由上面的代碼運(yùn)行結(jié)果可知await并不是針對于async的方法,而是針對async方法所返回給我們的Task ,這也是為什么await必須返回一個(gè)Task,所以await也可以用來修飾一個(gè)task對象,告訴編譯器需要等這個(gè)task執(zhí)行完畢才會往下走。
static void Main(string[] args)
{
Console.WriteLine("當(dāng)前是主線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine( DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
var task = Task.Run(() =>
{
return GetName();
});
Console.WriteLine("主線程繼續(xù)執(zhí)行:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
task.GetAwaiter().OnCompleted(() =>
{
// 2 秒之后才會執(zhí)行這里
var name = task.Result;
Console.WriteLine("My name is: " name DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
});
}
static string GetName()
{
Console.WriteLine("執(zhí)行g(shù)etname方法Current Thread Id :{0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
Console.WriteLine("In antoher thread.....");
return "Shirley";
}
static async Task<string> GetNameAsync()
{
Console.WriteLine("GetNameAsync中delay之前, current thread Id is: {0}", Thread.CurrentThread.ManagedThreadId);
await Task.Delay(2000);
// 這里還是主線程
Console.WriteLine("GetNameAsync中delay之後, current thread Id is: {0}", Thread.CurrentThread.ManagedThreadId);
return await Task.Run(() =>
{
Console.WriteLine("'GetName' Thread Id: {0}", Thread.CurrentThread.ManagedThreadId);
return "Shirley";
});
}
運(yùn)行結(jié)果:

發(fā)現(xiàn)一件有意思的事情,也不知道是不是因?yàn)榫€程池的原因還是我理解的哪里有問題,我理解的是await task.delay 是會開啟新線程的。我把上面Main代碼中的GetName換成GetNameAsync ,運(yùn)行后task.delay的前后線程沒有變化
我是主線程:Thread Id 10
當(dāng)前是主線程:Thread Id 10
2019-06-23 17:38:44
主線程繼續(xù)執(zhí)行:Thread Id 10
主線程結(jié)束
GetNameAsync中delay之前, current thread Id is: 6
GetNameAsync中delay之後, current thread Id is: 6
'GetName' Thread Id: 11
My name is: Jesse2019-06-23 17:38:46
但如果我在main中其他代碼刪除直接執(zhí)行GetNameAsync() ,task.delay前后的線程不是同一個(gè)會有變更,如下所示:
我是主線程:Thread Id 9
當(dāng)前是主線程:Thread Id 9
2019-06-23 17:37:56
GetNameAsync中delay之前, current thread Id is: 9
主線程繼續(xù)執(zhí)行:Thread Id 9
主線程結(jié)束
GetNameAsync中delay之後, current thread Id is: 11
'GetName' Thread Id: 12
將上一節(jié)內(nèi)容中Main代碼中的task.GetAwaiter().OnCompleted 方法整個(gè)去掉用
Console.WriteLine("My name is: " task.Result DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); 來替代,執(zhí)行結(jié)果如下:
我是主線程:Thread Id 10
當(dāng)前是主線程:Thread Id 10
2019-06-23 17:43:28
主線程繼續(xù)執(zhí)行:Thread Id 10
GetNameAsync中delay之前, current thread Id is: 6
GetNameAsync中delay之後, current thread Id is: 6
'GetName' Thread Id: 11
My name is: Jesse2019-06-23 17:43:30
主線程結(jié)束
對比結(jié)果得到如下總結(jié):
- 加上await關(guān)鍵字之后,后面的代碼會被掛起等待,直到task執(zhí)行完畢有返回值的時(shí)候才會繼續(xù)向下執(zhí)行,這一段時(shí)間主線程會處于掛起狀態(tài)。
- GetAwaiter方法會返回一個(gè)awaitable的對象(繼承了INotifyCompletion.OnCompleted方法)我們只是傳遞了一個(gè)委托進(jìn)去,等task完成了就會執(zhí)行這個(gè)委托,但是并不會影響主線程,下面的代碼會立即執(zhí)行。這也是為什么兩段代碼結(jié)果輸出不一樣!
static void Main(string[] args)
{
Console.WriteLine("當(dāng)前是主線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine( DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
var task = Task.Run(() =>
{
return GetNameAsync();
});
var name = task.GetAwaiter().GetResult();
Console.WriteLine("My name is: " task.Result DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine("主線程結(jié)束");
}
Task.GetAwait()方法會給我們返回一個(gè)TaskAwaiter的對象,通過調(diào)用這個(gè)對象的GetResult方法就會掛起主線程,當(dāng)然也不是所有的情況都會掛起。還記得我們Task的特性么? 在一開始的時(shí)候就啟動了另一個(gè)線程去執(zhí)行這個(gè)Task,當(dāng)我們調(diào)用它的結(jié)果的時(shí)候如果這個(gè)Task已經(jīng)執(zhí)行完畢,主線程是不用等待可以直接拿其結(jié)果的,如果沒有執(zhí)行完畢那主線程就得掛起等待了。
如下代碼:
// 這里主線程會掛起等待,直到task執(zhí)行完畢我們拿到返回結(jié)果
var name = task.GetAwaiter().GetResult();
// 這里不會掛起等待,因?yàn)閠ask已經(jīng)執(zhí)行完了,我們可以直接拿到結(jié)果
var nameawait = await task;
共享數(shù)據(jù)
private static bool _isDone = false;
static void Main(string[] args)
{
new Thread(Done).Start();
new Thread(Done).Start();
}
static void Done()
{
Console.WriteLine("我是線程:Thread Id {0}",Thread.CurrentThread.ManagedThreadId);
if (!_isDone)
{
_isDone = true; // 第二個(gè)線程來的時(shí)候,就不會再執(zhí)行了(也不是絕對的,取決于計(jì)算機(jī)的CPU數(shù)量以及當(dāng)時(shí)的運(yùn)行情況)
Console.WriteLine("這裡應(yīng)該只執(zhí)行一次");
}
else
{
Console.WriteLine("已經(jīng) 執(zhí)行過");
}
}
運(yùn)行結(jié)果

線程之間可以通過static變量來共享數(shù)據(jù)。
線程安全
private static bool _isDone = false;
static void Main(string[] args)
{
new Thread(Done).Start();
new Thread(Done).Start();
}
static void Done()
{
Console.WriteLine("我是線程:Thread Id {0}",Thread.CurrentThread.ManagedThreadId);
if (!_isDone)
{
Console.WriteLine("這裡應(yīng)該只執(zhí)行一次");//這就從上面的代碼的從後面移到前面,就會執(zhí)行兩次,線程不安全
_isDone = true;
}
else
{
Console.WriteLine("已經(jīng) 執(zhí)行過");
}
}
運(yùn)行結(jié)果:

上面這種情況并不是一直發(fā)生,運(yùn)行結(jié)果是我打了斷點(diǎn)后出現(xiàn)的,如果不打斷點(diǎn)執(zhí)行可能就是正常的。出現(xiàn)的原因是第一個(gè)線程還沒有來得及把_isDone設(shè)置成true,第二個(gè)線程就進(jìn)來了,而這不是我們想要的結(jié)果,在多個(gè)線程下,結(jié)果不是我們的預(yù)期結(jié)果,這就是線程不安全。
鎖
在線程安全章節(jié)中,可能會出現(xiàn)線程不安全的情況,為了避免這種情況,可能就用到了鎖。鎖有很多分類:獨(dú)占鎖,互斥鎖,以及讀寫鎖等,下面代碼演示的是獨(dú)占鎖。
private static bool _isDone = false;
private static object _lock = new object();
static void Main(string[] args)
{
new Thread(DoneLock).Start();
new Thread(DoneLock).Start();
}
static void DoneLock()
{
Console.WriteLine("我是沒被鎖的內(nèi)容,可以並列執(zhí)行線程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
lock (_lock)
{
Console.WriteLine("我是鎖裡面的線程,其他線程執(zhí)行完才能被調(diào)用:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
if (!_isDone)
{
Console.WriteLine("我只執(zhí)行一次");
_isDone = true;
}
else
{
Console.WriteLine("已經(jīng)執(zhí)行了");
}
}
}
運(yùn)行結(jié)果:

在上鎖之后,在同一個(gè)時(shí)間內(nèi)只允許一個(gè)線程訪問,其他線程會被阻塞,只有等這個(gè)所被釋放后其他的線程才恩給你執(zhí)行被鎖住的代碼
上述代碼并不一定總是出現(xiàn)相同的結(jié)果,和電腦配置有一定關(guān)系,為了更好的展示鎖中代碼的執(zhí)行順序,是通過打斷點(diǎn)調(diào)試出來的結(jié)果。 鎖這塊的內(nèi)容很多,就不在這個(gè)地方過多闡述,后面有時(shí)間要把鎖這塊好好的研究一下。
Semaphore信號量
Semaphore負(fù)責(zé)協(xié)調(diào)線程,可以限制對某一資源訪問的線程數(shù)量,超過這個(gè)數(shù)量之后,其它的線程就得等待,只有等現(xiàn)在有線程釋放了之后,下面的線程才能訪問。這個(gè)跟鎖有相似的功能,只不過不是獨(dú)占的,它允許一定數(shù)量的線程同時(shí)訪問。
這里對SemaphoreSlim類的用法做一個(gè)簡單的例子:
運(yùn)行代碼:
private static SemaphoreSlim _sem = new SemaphoreSlim(3); // 3表示最多只能有三個(gè)線程同時(shí)訪問
static void Main(string[] args)
{
for (int i = 1; i <= 5; i )
{
new Thread(Enter).Start(i);
}
}
static void Enter(object id)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId " 開始排隊(duì)...");
_sem.Wait();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId " 開始執(zhí)行!");
Thread.Sleep(1000 * (int)id);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId " 執(zhí)行完畢,離開!");
_sem.Release();
}
運(yùn)行結(jié)果:

由上面的結(jié)果看出,一開始的三個(gè)線程并列執(zhí)行,后面的兩個(gè)線程,只有當(dāng)線程有執(zhí)行完畢的才會開始執(zhí)行。
總結(jié)
看了很多篇資料,自己也寫了一些測試用的代碼,也觀察了運(yùn)行結(jié)果,但是要想深入的了解還是需要在項(xiàng)目中切身去實(shí)踐。
總結(jié)一下個(gè)人感受吧,可能會有很多理解不到位甚至是錯(cuò)誤的地方,如果有誤請不吝指教:
- 能用Task 不用 Thread ,能用async和awai就最好選用這個(gè)。
- 并不是用了Task就一定會提高效率。
- async 和await 并不會開啟新線程,除非真正觸發(fā)了Async的方法,比如在await方法中開啟Task.Run
- 要用異步就全部用異步,比如async與await的使用,不要異步中穿插著同步方法
- 要結(jié)合具體需求,如果要使用Result或Wait的話,沒有搭配async和task是可以使用的,但是搭配用時(shí),如果是主控臺或者webservice中因?yàn)槠鹗键c(diǎn)不能用async和task,所以依然可以使用Result或Wait方式。但是如果是在MVC或WebAPI或者Winform中就會造成死鎖的問題。所以要善用async await語法糖,不過要遵循一個(gè)原則,就是從頭到尾都用async await 。
云里霧里,后續(xù)結(jié)合項(xiàng)目再好好理解一下吧。
參考文獻(xiàn)
這是第二篇發(fā)布出來的文章,除了自己學(xué)習(xí)之外,這篇文章也希望有懂這塊知識的伙伴能看見,能給與指導(dǎo),有些地方還是理解的不夠好。如果有錯(cuò)誤的地方,還請多多指教
來源:https://www./content-4-273151.html
|