CSharp中的Thread,Task,Async,Await,IAsyncResult理解


CSharp中的Thread,Task,Async,Await,IAsyncResult理解


1. 線程(Thread)

多線程的意義在於一個應用程序中,有多個執行部分可以同時執行;對於比較耗時的操作(例如io,數據庫操作),或者等待響應(如WCF通信)的操作,可以單獨開啟后台線程來執行,這樣主線程就不會阻塞,可以繼續往下執行;等到后台線程執行完畢,再通知主線程,然后做出對應操作!

在C#中開啟一個新線程來執行一個耗時任務比較簡單,代碼如下:

static void Main(string[] args) {
     Console.WriteLine("主線程開始");
	 
     //IsBackground=true,將其設置為后台線程
     Thread t = new Thread(Run) { IsBackground = true };
     t.Start();
	 
     Console.WriteLine("主線程在做其他的事!");
	 
     //主線程結束,后台線程會自動結束,不管有沒有執行完成
     //Thread.Sleep(300);
     Thread.Sleep(1500);
     Console.WriteLine("主線程結束");
}

static void Run() {
	 Thread.Sleep(700);
	 Console.WriteLine("這是后台線程調用");
}

執行結果如下圖:
執行結果

我們可以根據執行結果看出,在啟動后台線程之后,主線程繼續往下執行了,並沒有等到后台線程執行完之后,再向下執行.

1.1 線程池

試想一下,如果有大量的任務需要處理,例如網站后台對於HTTP請求的處理,那是不是要對每一個請求創建一個后台線程呢?顯然不合適,這會占用大量內存,而且頻繁地創建的過程也會嚴重影響速度,那怎么辦呢?線程池就是為了解決這一問題,把創建的線程存起來,形成一個線程池(里面有多個線程),當要處理任務時,若線程池中有空閑線程(前一個任務執行完成后,線程不會被回收,會被設置為空閑狀態),則直接調用線程池中的線程執行(例asp.net處理機制中的Application對象).

代碼舉例:

for(int i=0;i<10;I++)
{
	ThreadPool.QuequeUserWorkItem(m=>
	{
		Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString());
	});
}

運行結果:

運行結果
運行結果

可以看到我們雖然執行了10次,但是並沒有創建10個線程,由此我們可以得出在處理簡單的耗時任務時,我們可以使用線程池技術來處理,而不是手動的去開辟線程來處理耗時任務.

1.2 信號量(Semaphore)

Semaphore負責協調線程,可以限制對某一資源訪問的線程數量,
下面是對SemaphoreSlim類的用法的簡單描述:

static SemaphoreSlim semLim = new SemaphoreSlim(3); //3表示最多只能有三個線程同時訪問
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() + "開始執行");
	 Thread.Sleep(2000);
	 Console.WriteLine("線程" + Thread.CurrentThread.ManagedThreadId.ToString() + "執行完畢");
	 semLim.Release();
}

執行結果:

初始狀態
初始狀態

運行一段時間之后
運行一段時間之后

可以看到,剛開始只有三個線程在執行,當一個線程執行完畢並釋放之后,才會有新的線程來執行方法.

除了使用SemaphoreSlim類,還可以使用Semaphore類,感覺更加靈活,下面舉例:


2.Task

跟線程池ThreadPool的功能類似,但是更加方便,常常搭配 async,以及await關鍵字一起使用,用Task開啟新任務時,會從線程池中調用閑置線程來執行任務,演示代碼如下:

Console.WriteLine("主線程啟動");

//Task.Run啟動一個線程池中的線程

//Task啟動的是后台線程,要在主線程中等待后台線程執行完畢,可以調用Wait方法,Wait方法會阻塞當前線程,等待task啟動的耗時任務結束.

//Task task = Task.Factory.StartNew(() => { Thread.Sleep(1500); Console.WriteLine("task啟 動"); });

Task task = Task.Run(() => { 
 Thread.Sleep(1500);
 Console.WriteLine("task啟動");
});
Thread.Sleep(300);
task.Wait();
Console.WriteLine("主線程結束");

執行結果如下:

執行結果
執行結果

開啟新任務的方法:

Task.Run();
//或者
Task.Factory.StartNew();

開始的是后台線程,要在主線程中等待后台線程執行完畢,可以使用Wait方法(會以同步的方式來執行).不用Wait則會以異步方式來執行.

下面使用代碼來比較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調用的Thread Id =" + Thread.CurrentThread.ManagedThreadId);
}

執行結果:

執行結果
執行結果

我們可以看出使用Thread會開啟5個線程,用Task只開啟了3個線程.

2.1 Task<TResult>

Task <TResult>就是有返回值的Task, TResult就是返回值類型.

示例代碼:

Console.WriteLine("主線程開始");

//返回值類型為string
Task<string> task = Task<string>.Run(() => {
 Thread.Sleep(2000); 
 return Thread.CurrentThread.ManagedThreadId.ToString(); 
});

//會等到task執行完畢才會輸出;
Console.WriteLine(task.Result);

Console.WriteLine("主線程結束");

運行結果:

運行結果
運行結果

根據運行結果,我們可以看出 task.Result方法,會阻塞當前線程,等待 Task 任務執行之后,返回函數運算結果之后,才繼續向下執行.

注: Task任務可以通過CancellationTokenSource類來控制是否取消執行,下面演示CancellationTokenSource類的用法:


  • 演示:

3. async/await 關鍵字

async關鍵字用來修飾方法,表明這個方法是異步的,聲明的方法的返回類型必須為:void,Task或者Task <TResult>. 並且按照規范,使用async關鍵字修改的方法名應該用Async結尾, 如 GetEmployeesAsync

await 關鍵字必須用來修飾Task或者 Task <TResult> ,而且只能出現在已經用 async 關鍵字修飾的異步方法中,通常情況下, async/await成對出現才有意義.

示例代碼:


static void Main(string[] args) {
 	Console.WriteLine("-------主線程啟動-------");
	
 	Task<int> task = GetStrLengthAsync();
	
 	Console.WriteLine("主線程繼續執行");
	
 	Console.WriteLine("Task返回的值" + task.Result);
	
 	Console.WriteLine("-------主線程結束-------");
}

static async Task<int> GetStrLengthAsync()
{
 	Console.WriteLine("GetStrLengthAsync方法開始執行");
	
 	//此處返回的<string>中的字符串類型,而不是Task<string>
 	string str = await GetString();
 	Console.WriteLine("GetStrLengthAsync方法執行結束");
 	return str.Length;
}

static Task<string> GetString()
{
	//Console.WriteLine("GetString方法開始執行")
 	return Task<string>.Run(() =>
 	{
 		Thread.Sleep(2000);
 		return "GetString的返回值";
 	});
}

運行結果:

運行結果
運行結果

可以看出,main函數調用 GetStrLengthAsync 方法后,在await之前,都是同步執行到,遇到await關鍵字之后,主線程才會從GetStrLengthAsync退出來繼續往下執行.

那么是否在遇到await關鍵字的時候程序自動開啟了一個后台線程去執行GetString方法呢?

現在把GetString方法中的那行注釋解除,運行結果如下:

運行結果
運行結果

大家可以看到,在遇到await關鍵字后,沒有繼續執行GetStrLengthAdync方法后面的操作,也沒有馬上返回到main方法,而是執行了GetString的第一行,以此可以判斷await這里並額米有開啟新的線程去執行GetString方法,而是以同步的方式讓GetString方法執行,等到執行GetString方法中的Task<string>.Run()的時候才由Task開啟了后台線程!

那么await的作用是什么呢?

可以從字面上理解,上面提到task.wait可以讓主線程等待后台線程執行完畢,await和wait類似,同樣是等待,等待Task<string>.Run()開始的后台線程執行完畢,不同的是await不會阻塞主線程,只會讓GetStrLengthAsync方法暫停執行。

那么await是怎么做到的呢?有沒有開啟新線程去等待?

運行分析
運行分析

只有兩個線程(主線程和Task開啟的線程)!至於怎么做到的后續在進行深入研究.

4. IAsyncResult

包含可異步操作的方法的類需要實現IAsyncResult接口,Task類就實現了此接口.

反編譯結果
反編譯結果

在不借助Task的情況下怎么實現異步呢?
一種方法是:我們可以使用委托的方式,下面來描述一下另一種方式

class Program {
 	static void Main(string[] args) {
		 Console.WriteLine("主程序開始--------------------");
		 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} 正在運行.",Thread.CurrentThread.ManagedThreadId)
		 //會阻塞線程,直到后台線程執行完畢之后,才會往下執行
		 result.AsyncWaitHandle.WaitOne();                                                          //關鍵步驟
		 Console.WriteLine("主程序在做一些事情!!!");
		 
		 //獲取異步執行的結果
		 string returnValue = caller.EndInvoke(out threadId, result);                      //關鍵步驟
		 
		 //釋放資源
		 result.AsyncWaitHandle.Close();
		 Console.WriteLine("主程序結束--------------------");
		 Console.Read();
 	}
}
public class AsyncDemo {
	 //供后台線程執行的方法
 	public string TestMethod(int callDuration, out int threadId) {
		 Console.WriteLine("測試方法開始執行.");
		 Thread.Sleep(callDuration);
		 threadId = Thread.CurrentThread.ManagedThreadId;
		 return String.Format("測試方法執行的時間 {0}.", callDuration.ToString());
	 }
}

public delegate string AsyncMethodCaller(int callDuration, out int threadId);

運行結果:

運行結果
運行結果

和Task的用法差異不是很大!result.AsyncWaitHandle.WaitOne()就類似Task的Wait。

5. Parallel

5.1 循環例子

Parallel靜態類,提供了可以在循環中開啟線程的方法,示例代碼如下:

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);

//下面的代碼是使用Parallel.For循環在循環的過程中開啟線程

Stopwatch watch2 = new Stopwatch();
watch2.Start();

//會調用線程池中的線程
Parallel.For(1, 11, i =>
{
 Console.WriteLine(i + ",線程ID:" + Thread.CurrentThread.ManagedThreadId);
 Thread.Sleep(1000);
});
watch2.Stop();

Console.WriteLine(watch2.Elapsed);

運行結果:

運行結果
運行結果

5.2 循環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);
});

5.3 執行Action[]數組中的方法

Action[] actions = new Action[]
{ 
 	new Action(()=>
	{
 		Console.WriteLine("方法1");
 	}),
	
 	new Action(()=>{
 		Console.WriteLine("方法2");
 	})
};

Parallel.Invoke(actions);

6. 異步回調

文中所有Task<TResult>的返回值都是直接用task.result獲取,這樣如果后台任務沒有執行完畢的話,主線程會等待其執行完畢,這樣的話就和同步一樣了(看上去一樣,但其實await的時候並不會造成線程的阻塞,web程序感覺不到,但是wpf,winform這樣的桌面程序若不使用異步,會造成UI線程的阻塞)。簡單演示一下Task回調函數的使用:

Console.WriteLine("主線程開始");

Task<string> task = Task<string>.Run(() => {
	Thread.Sleep(2000); 
 	return Thread.CurrentThread.ManagedThreadId.ToString(); 
});

//會等到任務執行完之后執行
task.GetAwaiter().OnCompleted(() =>
{
 	Console.WriteLine(task.Result);
});

Console.WriteLine("主線程結束");
Console.Read();

執行結果:

執行結果
執行結果

OnCompleted中的代碼會在任務執行完成之后執行,另外ContinueWith也是一個重要的方法:

Console.WriteLine("主線程開始");

Task<string> task = Task<string>.Run(() => {
 	Thread.Sleep(2000); 
 return Thread.CurrentThread.ManagedThreadId.ToString(); 
});

task.GetAwaiter().OnCompleted(() =>
{
 	Console.WriteLine(task.Result);
});

task.ContinueWith(m=>{

	Console.WriteLine("第一個任務結束啦!我是第二個任務");
});
Console.WriteLine("主線程結束");
Console.Read();

執行結果:

執行結果
執行結果

ContinueWith(); 方法可以讓該后台線程繼續執行新的任務.

7. 委托方式實現異步

可以參考以下博文:
委托方式實現方法的同步執行,異步執行,異步回調


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM