1. 程式人生 > >走進非同步世界async、await

走進非同步世界async、await

此處少了一張圖:不過無傷大雅!

上述程式碼是對真實案例的簡化,我點執行之後,發現執行到HttpResponseMessage httpResp = await httpClient.PostAsJsonAsync(reqUrl, t);紅色小箭頭進入到這句程式碼之後就消失的無影無蹤,我等了半宿,然後……然後就沒有然後了,沒有異常,只有寂寞。

關於async和await,這兄弟倆是對非同步程式設計的語法簡化。談到非同步,就涉及到執行緒和邏輯執行順序,看下面程式碼就一清二楚了。

class Program
{
static void Main(string[] args)
{
Console.WriteLine("step1,執行緒ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

AsyncDemo demo = new AsyncDemo();
//demo.AsyncSleep().Wait();//Wait會阻塞當前執行緒直到AsyncSleep返回
demo.AsyncSleep();//不會阻塞當前執行緒

Console.WriteLine("step5,執行緒ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
Console.ReadLine();
}
}

public class AsyncDemo
{

public async Task AsyncSleep()
{
Console.WriteLine("step2,執行緒ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

//await關鍵字表示“等待”Task.Run傳入的邏輯執行完畢,此時(等待時)AsyncSleep的呼叫方能繼續往下執行(準確地說,是當前執行緒不會被阻塞)
//Task.Run將開闢一個新執行緒執行指定邏輯
await Task.Run(() => Sleep(10));

Console.WriteLine("step4,執行緒ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
}

private void Sleep(int second)
{
Console.WriteLine("step3,執行緒ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

Thread.Sleep(second * 1000);
}

}

執行結果:

注意step2和step4雖然在同一個方法內部,但它們的執行執行緒是不同的,step4與step3一樣使用Task.Run開闢的新執行緒。注意:假如我們在Sleep裡再次使用Task.Run又開闢了新執行緒,假設ID為10,並通過await關鍵詞修飾,那麼step4將執行線上程10。假如將第行註釋互換:

demo.AsyncSleep().Wait();
Wait會阻塞當前執行緒直到AsyncSleep返回
2 demo.AsyncSleep();
不會阻塞當前執行緒
即人為控制非同步邏輯同步返回,其實這和之前獲取使用者資訊的場景是一樣一樣的,猜想是在執行step2或step3後再無後續輸出。
執行結果:

看來“事與願違”。那麼之前的出現的問題是怎麼回事呢?既然step4和step1所線上程不一樣,我們能想到什麼?當然是執行緒死鎖了!
提問:再將程式碼改為Task.Run(() => Sleep(10)).Wait();這時候會輸出什麼呢,或者說step4的輸出執行緒ID是多少?Task.Wait();和await不一樣,它會阻塞當前執行緒(而不管內部邏輯是否開闢了新的執行緒)。
執行結果:

可得step4仍執行在主執行緒。

執行緒死鎖
引起執行緒死鎖的原因有很多。在ASP.NET[ MVC]的場景中,涉及到一個概念就是AspNetSynchronizationContext。AspNetSynchronizationContext出現在.NET Framework 2.0中,因為這個版本在 ASP.NET 體系結構中引入了非同步頁面。在 .NET Framework 2.0 之前的版本中,每個 ASP.NET 請求都需要一個執行緒,直到該請求完成。 這會造成執行緒利用率低下,因為頁面邏輯通常依賴於資料庫查詢和 Web 服務呼叫,並且處理請求的執行緒必須等待,直到所有這些操作結束。 使用非同步頁面,處理請求的執行緒可以開始每個操作,然後返回到 ASP.NET 執行緒池,當操作結束時,ASP.NET 執行緒池的另一個執行緒可以完成該請求,AspNetSynchronizationContext在這個過程中扮演了非同步操作週期維護員的角色(或許還發揮了其它作用)。當一個非同步操作完成,需要依賴AspNetSynchronizationContext告知頁面,此時AspNetSynchronizationContext將未完成的非同步運算元減1,並以同步方式處理非同步執行緒傳送過來的委託(即便是以Post“非同步”方法),因此假如一個頁面請求有多個非同步操作同時完成,每次也只能執行一個回撥委託(不同委託執行的執行緒不知是否是同一個,however,執行執行緒將具有原始頁面的標識和區域)。綜上所述,同一個AspNetSynchronizationContext(不知道一個AspNetSynchronizationContext例項是針對單個請求還是整個應用程式)同時只能最多被一個執行緒使用,結合async和await的特性,回到本文開頭的程式碼:
SynchronizationContext 綜述

public ActionResult Index()
{
//執行緒A阻塞,等待GetUserInfo返回,當前上下文AspNetSynchronizationContext
IEnumerable<string> a = HttpHelper.GetjsonAsync<IEnumerable<string>>("http://localhost:13817/api/Values").Result;

return View();
}

public async static Task<TResult> PostJsonAsync<TData, TResult>(string reqUrl, TData t)
{
// PostAsJsonAsync在其內部開闢新執行緒(設為B)非同步執行,注意await並不會阻塞當前執行緒,而是將控制權返回方法呼叫方,這裡是Index Action
HttpResponseMessage httpResp = await httpClient.PostAsJsonAsync(reqUrl, t);

//PostAsJsonAsync返回,但下列程式碼仍執行線上程B。當前方法企圖重入AspNetSynchronizationContext,死鎖產生在這裡

if (httpResp.IsSuccessStatusCode)
{
return await httpResp.Content.ReadAsAsync<TResult>();
}
else
{
return default(TResult);
}
}

後記
await關鍵字並不表示後續程式碼馬上在新執行緒上執行,是否開闢執行緒取決於是否真正建立了Task(or 從Task池中取得)。執行下面程式碼:

class Program
{
static void Main(string[] args)
{
Console.WriteLine($"1:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
TestTransfer1();
Console.WriteLine($"8:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
Console.ReadLine();
}

static async void TestTransfer1()
{
Console.WriteLine($"2:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
await TestTransfer2();
Console.WriteLine($"7:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
}

static async Task TestTransfer2()
{
Console.WriteLine($"3:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
await Test();
Console.WriteLine($"6:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
}

static async Task Test()
{
Console.WriteLine($"4:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() => Sleep(5)); //此處之後才開闢了新執行緒
Console.WriteLine($"5:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
}

static void Sleep(int second)
{
Thread.Sleep(second * 1000);
}
}

執行結果:

 

 

一目瞭然,所以我們不需要擔心多級方法呼叫時會建立眾多執行緒並切換導致的效能問題。

關於是否在await後才開始真正執行非同步方法,改造上面程式碼如下:

class Program
{
static void Main(string[] args)
{
TestTransfer1();
Console.ReadLine();
}

static async void TestTransfer1()
{
Console.WriteLine($"1:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
var task = Test();
Sleep(2);
Console.WriteLine($"4:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
await task;
}

static async Task Test()
{
Console.WriteLine($"2:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() => Sleep(1)); //此處之後才開闢了新執行緒
Console.WriteLine($"3:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
}

static void Sleep(int second)
{
Thread.Sleep(second * 1000);
}
}

執行結果:

可知在獲取task例項時,非同步操作就開始了,而不需要等await。由於這個特性,我們可以發起多個沒有順序依賴關係的task,最後再統一await它們,提高效率,比如分頁:

var task_totalcount = query.CountAsync();
query = query.OrderBy(sortfield, sortorder);
query = query.Skip(startindex).Take(takecount);
var task_getdata = query.ToListAsync();

result.TotalCount = await task_totalcount;
result.Data = await task_getdata;

return result;