C#中 Thread,Task,Async/Await,IAsyncResult 的那些事兒!

提及異步,Thread,Task,async/await,IAsyncResult 這些東西確定是繞不開的,今天就來依次聊聊他們web

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("這是後臺線程調用"); }

 執行結果以下圖,多線程

能夠看到在啓動後臺線程以後,主線程繼續往下執行了,並無等到後臺線程執行完以後。asp.net

1.1 線程池

試想一下,若是有大量的任務須要處理,例如網站後臺對於HTTP請求的處理,那是否是要對每個請求建立一個後臺線程呢?顯然不合適,這會佔用大量內存,並且頻繁地建立的過程也會嚴重影響速度,那怎麼辦呢?線程池就是爲了解決這一問題,把建立的線程存起來,造成一個線程池(裏面有多個線程),當要處理任務時,若線程池中有空閒線程(前一個任務執行完成後,線程不會被回收,會被設置爲空閒狀態),則直接調用線程池中的線程執行(例asp.net處理機制中的Application對象),異步

使用事例:async

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

運行結果:函數

能夠看到,雖然執行了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

Task是.NET4.0加入的,跟線程池ThreadPool的功能相似,用Task開啓新任務時,會從線程池中調用線程,而Thread每次實例化都會建立一個新的線程。

Console.WriteLine("主線程啓動");
//Task.Run啓動一個線程
//Task啓動的是後臺線程,要在主線程中等待後臺線程執行完畢,能夠調用Wait方法
//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任務能夠經過CancellationTokenSource類來取消,感受用得很少,用法比較簡單,感興趣的話能夠搜一下!

 3. async/await

async/await是C#5.0中推出的,先上用法:

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的返回值"; }); }

async用來修飾方法,代表這個方法是異步的,聲明的方法的返回類型必須爲:void,Task或Task<TResult>。

await必須用來修飾Task或Task<TResult>,並且只能出如今已經用async關鍵字修飾的異步方法中。一般狀況下,async/await成對出現纔有意義,

看看運行結果:

能夠看出來,main函數調用GetStrLengthAsync方法後,在await以前,都是同步執行的,直到遇到await關鍵字,main函數才返回繼續執行。

那麼是不是在遇到await關鍵字的時候程序自動開啓了一個後臺線程去執行GetString方法呢?

如今把GetString方法中的那行註釋加上,運行的結果是:

你們能夠看到,在遇到await關鍵字後,沒有繼續執行GetStrLengthAsync方法後面的操做,也沒有立刻反回到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自.NET1.1起就有了,包含可異步操做的方法的類須要實現它,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

最後說一下在循環中開啓多線程的簡單方法:

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

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

運行結果:

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

執行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中的代碼會在任務執行完成以後執行!

另外task.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()方法可讓該後臺線程繼續執行新的任務。

Task的使用仍是比較靈活的,你們能夠研究下,好了,以上就是所有內容了,篇幅和能力都有限,但願對你們有用!

相關文章
相關標籤/搜索