詳解C#中 Thread,Task,Async/Await,IAsyncResult的那些事兒
說(shuō)起異步,Thread,Task,async/await,IAsyncResult 這些東西肯定是繞不開(kāi)的,今天就來(lái)依次聊聊他們
1.線程(Thread)
多線程的意義在于一個(gè)應(yīng)用程序中,有多個(gè)執(zhí)行部分可以同時(shí)執(zhí)行;對(duì)于比較耗時(shí)的操作(例如io,數(shù)據(jù)庫(kù)操作),或者等待響應(yīng)(如WCF通信)的操作,可以單獨(dú)開(kāi)啟后臺(tái)線程來(lái)執(zhí)行,這樣主線程就不會(huì)阻塞,可以繼續(xù)往下執(zhí)行;等到后臺(tái)線程執(zhí)行完畢,再通知主線程,然后做出對(duì)應(yīng)操作!
在C#中開(kāi)啟新線程比較簡(jiǎn)單
static void Main(string[] args)
{
Console.WriteLine("主線程開(kāi)始");
//IsBackground=true,將其設(shè)置為后臺(tái)線程
Thread t = new Thread(Run) { IsBackground = true };
t.Start();
Console.WriteLine("主線程在做其他的事!");
//主線程結(jié)束,后臺(tái)線程會(huì)自動(dòng)結(jié)束,不管有沒(méi)有執(zhí)行完成
//Thread.Sleep(300);
Thread.Sleep(1500);
Console.WriteLine("主線程結(jié)束");
}
static void Run()
{
Thread.Sleep(700);
Console.WriteLine("這是后臺(tái)線程調(diào)用");
}
執(zhí)行結(jié)果如下圖,

可以看到在啟動(dòng)后臺(tái)線程之后,主線程繼續(xù)往下執(zhí)行了,并沒(méi)有等到后臺(tái)線程執(zhí)行完之后。
1.1 線程池
試想一下,如果有大量的任務(wù)需要處理,例如網(wǎng)站后臺(tái)對(duì)于HTTP請(qǐng)求的處理,那是不是要對(duì)每一個(gè)請(qǐng)求創(chuàng)建一個(gè)后臺(tái)線程呢?顯然不合適,這會(huì)占用大量?jī)?nèi)存,而且頻繁地創(chuàng)建的過(guò)程也會(huì)嚴(yán)重影響速度,那怎么辦呢?線程池就是為了解決這一問(wèn)題,把創(chuàng)建的線程存起來(lái),形成一個(gè)線程池(里面有多個(gè)線程),當(dāng)要處理任務(wù)時(shí),若線程池中有空閑線程(前一個(gè)任務(wù)執(zhí)行完成后,線程不會(huì)被回收,會(huì)被設(shè)置為空閑狀態(tài)),則直接調(diào)用線程池中的線程執(zhí)行(例asp.net處理機(jī)制中的Application對(duì)象),
使用事例:
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem(m =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString());
});
}
Console.Read();
運(yùn)行結(jié)果:

可以看到,雖然執(zhí)行了10次,但并沒(méi)有創(chuàng)建10個(gè)線程。
1.2 信號(hào)量(Semaphore)
Semaphore負(fù)責(zé)協(xié)調(diào)線程,可以限制對(duì)某一資源訪問(wèn)的線程數(shù)量
這里對(duì)SemaphoreSlim類(lèi)的用法做一個(gè)簡(jiǎn)單的事例:
static SemaphoreSlim semLim = new SemaphoreSlim(3); //3表示最多只能有三個(gè)線程同時(shí)訪問(wèn)
static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
new Thread(SemaphoreTest).Start();
}
Console.Read();
}
static void SemaphoreTest()
{
semLim.Wait();
Console.WriteLine("線程" + Thread.CurrentThread.ManagedThreadId.ToString() + "開(kāi)始執(zhí)行");
Thread.Sleep(2000);
Console.WriteLine("線程" + Thread.CurrentThread.ManagedThreadId.ToString() + "執(zhí)行完畢");
semLim.Release();
}
執(zhí)行結(jié)果如下:


可以看到,剛開(kāi)始只有三個(gè)線程在執(zhí)行,當(dāng)一個(gè)線程執(zhí)行完畢并釋放之后,才會(huì)有新的線程來(lái)執(zhí)行方法!
除了SemaphoreSlim類(lèi),還可以使用Semaphore類(lèi),感覺(jué)更加靈活,感興趣的話(huà)可以搜一下,這里就不做演示了!
2.Task
Task是.NET4.0加入的,跟線程池ThreadPool的功能類(lèi)似,用Task開(kāi)啟新任務(wù)時(shí),會(huì)從線程池中調(diào)用線程,而Thread每次實(shí)例化都會(huì)創(chuàng)建一個(gè)新的線程。
Console.WriteLine("主線程啟動(dòng)");
//Task.Run啟動(dòng)一個(gè)線程
//Task啟動(dòng)的是后臺(tái)線程,要在主線程中等待后臺(tái)線程執(zhí)行完畢,可以調(diào)用Wait方法
//Task task = Task.Factory.StartNew(() => { Thread.Sleep(1500); Console.WriteLine("task啟動(dòng)"); });
Task task = Task.Run(() => {
Thread.Sleep(1500);
Console.WriteLine("task啟動(dòng)");
});
Thread.Sleep(300);
task.Wait();
Console.WriteLine("主線程結(jié)束");
執(zhí)行結(jié)果如下:

開(kāi)啟新任務(wù)的方法:Task.Run()或者Task.Factory.StartNew(),開(kāi)啟的是后臺(tái)線程
要在主線程中等待后臺(tái)線程執(zhí)行完畢,可以使用Wait方法(會(huì)以同步的方式來(lái)執(zhí)行)。不用Wait則會(huì)以異步的方式來(lái)執(zhí)行。
比較一下Task和Thread:
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
new Thread(Run1).Start();
}
for (int i = 0; i < 5; i++)
{
Task.Run(() => { Run2(); });
}
}
static void Run1()
{
Console.WriteLine("Thread Id =" + Thread.CurrentThread.ManagedThreadId);
}
static void Run2()
{
Console.WriteLine("Task調(diào)用的Thread Id =" + Thread.CurrentThread.ManagedThreadId);
}
執(zhí)行結(jié)果:

可以看出來(lái),直接用Thread會(huì)開(kāi)啟5個(gè)線程,用Task(用了線程池)開(kāi)啟了3個(gè)!
2.1 Task<TResult>
Task<TResult>就是有返回值的Task,TResult就是返回值類(lèi)型。
Console.WriteLine("主線程開(kāi)始");
//返回值類(lèi)型為string
Task<string> task = Task<string>.Run(() => {
Thread.Sleep(2000);
return Thread.CurrentThread.ManagedThreadId.ToString();
});
//會(huì)等到task執(zhí)行完畢才會(huì)輸出;
Console.WriteLine(task.Result);
Console.WriteLine("主線程結(jié)束");
運(yùn)行結(jié)果:

通過(guò)task.Result可以取到返回值,若取值的時(shí)候,后臺(tái)線程還沒(méi)執(zhí)行完,則會(huì)等待其執(zhí)行完畢!
簡(jiǎn)單提一下:
Task任務(wù)可以通過(guò)CancellationTokenSource類(lèi)來(lái)取消,感覺(jué)用得不多,用法比較簡(jiǎn)單,感興趣的話(huà)可以搜一下!
3. async/await
async/await是C#5.0中推出的,先上用法:
static void Main(string[] args)
{
Console.WriteLine("-------主線程啟動(dòng)-------");
Task<int> task = GetStrLengthAsync();
Console.WriteLine("主線程繼續(xù)執(zhí)行");
Console.WriteLine("Task返回的值" + task.Result);
Console.WriteLine("-------主線程結(jié)束-------");
}
static async Task<int> GetStrLengthAsync()
{
Console.WriteLine("GetStrLengthAsync方法開(kāi)始執(zhí)行");
//此處返回的<string>中的字符串類(lèi)型,而不是Task<string>
string str = await GetString();
Console.WriteLine("GetStrLengthAsync方法執(zhí)行結(jié)束");
return str.Length;
}
static Task<string> GetString()
{
//Console.WriteLine("GetString方法開(kāi)始執(zhí)行")
return Task<string>.Run(() =>
{
Thread.Sleep(2000);
return "GetString的返回值";
});
}
async用來(lái)修飾方法,表明這個(gè)方法是異步的,聲明的方法的返回類(lèi)型必須為:void,Task或Task<TResult>。
await必須用來(lái)修飾Task或Task<TResult>,而且只能出現(xiàn)在已經(jīng)用async關(guān)鍵字修飾的異步方法中。通常情況下,async/await成對(duì)出現(xiàn)才有意義,
看看運(yùn)行結(jié)果:

可以看出來(lái),main函數(shù)調(diào)用GetStrLengthAsync方法后,在await之前,都是同步執(zhí)行的,直到遇到await關(guān)鍵字,main函數(shù)才返回繼續(xù)執(zhí)行。
那么是否是在遇到await關(guān)鍵字的時(shí)候程序自動(dòng)開(kāi)啟了一個(gè)后臺(tái)線程去執(zhí)行GetString方法呢?
現(xiàn)在把GetString方法中的那行注釋加上,運(yùn)行的結(jié)果是:

大家可以看到,在遇到await關(guān)鍵字后,沒(méi)有繼續(xù)執(zhí)行GetStrLengthAsync方法后面的操作,也沒(méi)有馬上反回到main函數(shù)中,而是執(zhí)行了GetString的第一行,以此可以判斷await這里并沒(méi)有開(kāi)啟新的線程去執(zhí)行GetString方法,而是以同步的方式讓GetString方法執(zhí)行,等到執(zhí)行到GetString方法中的Task<string>.Run()的時(shí)候才由Task開(kāi)啟了后臺(tái)線程!
那么await的作用是什么呢?
可以從字面上理解,上面提到task.wait可以讓主線程等待后臺(tái)線程執(zhí)行完畢,await和wait類(lèi)似,同樣是等待,等待Task<string>.Run()開(kāi)始的后臺(tái)線程執(zhí)行完畢,不同的是await不會(huì)阻塞主線程,只會(huì)讓GetStrLengthAsync方法暫停執(zhí)行。
那么await是怎么做到的呢?有沒(méi)有開(kāi)啟新線程去等待?

只有兩個(gè)線程(主線程和Task開(kāi)啟的線程)!至于怎么做到的(我也不知道......>_<),大家有興趣的話(huà)研究下吧!
4.IAsyncResult
IAsyncResult自.NET1.1起就有了,包含可異步操作的方法的類(lèi)需要實(shí)現(xiàn)它,Task類(lèi)就實(shí)現(xiàn)了該接口

在不借助于Task的情況下怎么實(shí)現(xiàn)異步呢?
class Program
{
static void Main(string[] args)
{
Console.WriteLine("主程序開(kāi)始--------------------");
int threadId;
AsyncDemo ad = new AsyncDemo();
AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);
IAsyncResult result = caller.BeginInvoke(3000,out threadId, null, null);
Thread.Sleep(0);
Console.WriteLine("主線程線程 {0} 正在運(yùn)行.",Thread.CurrentThread.ManagedThreadId)
//會(huì)阻塞線程,直到后臺(tái)線程執(zhí)行完畢之后,才會(huì)往下執(zhí)行
result.AsyncWaitHandle.WaitOne();
Console.WriteLine("主程序在做一些事情?。?!");
//獲取異步執(zhí)行的結(jié)果
string returnValue = caller.EndInvoke(out threadId, result);
//釋放資源
result.AsyncWaitHandle.Close();
Console.WriteLine("主程序結(jié)束--------------------");
Console.Read();
}
}
public class AsyncDemo
{
//供后臺(tái)線程執(zhí)行的方法
public string TestMethod(int callDuration, out int threadId)
{
Console.WriteLine("測(cè)試方法開(kāi)始執(zhí)行.");
Thread.Sleep(callDuration);
threadId = Thread.CurrentThread.ManagedThreadId;
return String.Format("測(cè)試方法執(zhí)行的時(shí)間 {0}.", callDuration.ToString());
}
}
public delegate string AsyncMethodCaller(int callDuration, out int threadId);
關(guān)鍵步驟就是紅色字體的部分,運(yùn)行結(jié)果:

和Task的用法差異不是很大!result.AsyncWaitHandle.WaitOne()就類(lèi)似Task的Wait。
5.Parallel
最后說(shuō)一下在循環(huán)中開(kāi)啟多線程的簡(jiǎn)單方法:
Stopwatch watch1 = new Stopwatch();
watch1.Start();
for (int i = 1; i <= 10; i++)
{
Console.Write(i + ",");
Thread.Sleep(1000);
}
watch1.Stop();
Console.WriteLine(watch1.Elapsed);
Stopwatch watch2 = new Stopwatch();
watch2.Start();
//會(huì)調(diào)用線程池中的線程
Parallel.For(1, 11, i =>
{
Console.WriteLine(i + ",線程ID:" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
});
watch2.Stop();
Console.WriteLine(watch2.Elapsed);
運(yùn)行結(jié)果:

循環(huán)List<T>:
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 6, 7, 8, 9 };
Parallel.ForEach<int>(list, n =>
{
Console.WriteLine(n);
Thread.Sleep(1000);
});
執(zhí)行Action[]數(shù)組里面的方法:
Action[] actions = new Action[] {
new Action(()=>{
Console.WriteLine("方法1");
}),
new Action(()=>{
Console.WriteLine("方法2");
})
};
Parallel.Invoke(actions);
以上就是本文的全部?jī)?nèi)容,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,同時(shí)也希望多多支持腳本之家!
相關(guān)文章
C#使用默認(rèn)瀏覽器打開(kāi)網(wǎng)頁(yè)的方法
這篇文章主要介紹了C#使用默認(rèn)瀏覽器打開(kāi)網(wǎng)頁(yè)的方法,涉及C#操作瀏覽器屬性的相關(guān)技巧,需要的朋友可以參考下2015-06-06
Unity實(shí)現(xiàn)鼠標(biāo)拖動(dòng)3D物體
這篇文章主要為大家詳細(xì)介紹了Unity實(shí)現(xiàn)鼠標(biāo)拖動(dòng)3D物體,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10
C#進(jìn)階系列 WebApi身份認(rèn)證解決方案推薦:Basic基礎(chǔ)認(rèn)證
下面小編就為大家?guī)?lái)一篇C#進(jìn)階系列 WebApi身份認(rèn)證解決方案推薦:Basic基礎(chǔ)認(rèn)證。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家。給大家一個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-03-03
C# 如何實(shí)現(xiàn)一個(gè)基于值相等性比較的字典
這篇文章主要介紹了C# 如何實(shí)現(xiàn)一個(gè)基于值相等性比較的字典,幫助大家更好的理解和使用c#,感興趣的朋友可以了解下2021-02-02
C#實(shí)現(xiàn)對(duì)文件進(jìn)行加密解密的方法
這篇文章主要介紹了C#實(shí)現(xiàn)對(duì)文件進(jìn)行加密解密的方法,涉及C#加密與解密的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-04-04
C#編寫(xiě)游戲客戶(hù)端的實(shí)現(xiàn)代碼
這篇文章主要介紹了C#編寫(xiě)游戲客戶(hù)端的實(shí)現(xiàn)代碼,連接客戶(hù)端原理流程圖,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-11-11

