2012-02-26 32 views
63

的Techdays這裏在荷蘭的史蒂夫·桑德森在了一個約C#5, ASP.NET MVC 4, and asynchronous Web.爲什麼使用異步請求而不是使用較大的線程池?

他解釋說,當請求需要很長的時間才能完成,全部由線程池線程變得忙碌,新的請求必須等待演示。服務器無法處理負載,一切都變慢。

然後,他展示了異步web請求的使用如何提高性能,因爲工作被委派給另一個線程,並且線程池可以快速響應新的傳入請求。他甚至演示了這一點,並表明50個併發請求首先需要50 * 1個,但異步行爲總共只有1,2個。

但看到這個後,我仍然有一些問題。

  1. 爲什麼我們不能只使用一個更大的線程池?是不是使用異步/等待來調出另一個線程,然後從一開始就增加線程池?它不像我們運行的服務器突然得到更多線程或什麼東西?

  2. 來自用戶的請求仍在等待異步線程完成。如果池中的線程正在做其他事情,'UI'線程如何保持忙碌?史蒂夫提到了一些關於「知道什麼時候完成」的智能內核。這個怎麼用?

回答

55

這是一個很好的問題,理解它是瞭解異步IO如此重要的關鍵。新的異步/等待功能添加到C#5.0的原因是爲了簡化編寫異步代碼。但是,支持服務器上的異步處理並不新鮮,它自ASP.NET 2.0以來就存在。

就像史蒂夫告訴你的,通過同步處理,ASP.NET(和WCF)中的每個請求都從線程池獲取一個線程。他演示的問題是一個衆所周知的問題,稱爲「線程池飢餓」。如果您在服務器上創建同步IO,則線程池線程將在IO持續時間內保持阻塞狀態(無需執行任何操作)。由於線程池中的線程數量有限制,所以在負載下,這可能會導致所有線程池線程都被阻塞等待IO的情況,並且請求開始排隊,導致響應時間增加。由於所有線程都在等待IO完成,所以您將看到CPU佔用率接近0%(即使響應時間已經過去了)。

你在問什麼(爲什麼我們不能只使用一個更大的線程池?)是一個很好的問題。事實上,這是大多數人直到現在一直在解決線程池飢餓問題的方式:線程池中只有更多的線程。微軟的一些文檔甚至指出,修復線程池可能發生飢餓的情況。這是一個可以接受的解決方案,在C#5.0之前,要比重寫代碼完全異步更容易。

沒有與方法的幾個問題,但:

  • 有沒有價值,在所有情況下的工作原理:您將需要線程池中的線程數量線性依賴於持續時間IO和服務器上的負載。不幸的是,IO延遲大多是不可預測的。這裏是一個例子: 假設你在ASP.NET應用程序中向第三方Web服務發出HTTP請求,這需要大約2秒的時間才能完成。你會遇到線程池餓死的情況,所以你決定增加線程池的大小,比如說200個線程,然後它再次開始工作。問題是,也許下週,網絡服務將出現技術問題,將響應時間提高到10秒。突然之間,線程池飢餓又回來了,因爲線程被阻塞了5倍,所以你現在需要增加5次,到1,000線程。

  • 可擴展性和性能:第二個問題是,如果你這樣做,你仍然會爲每個請求使用一個線程。線程是一個昂貴的資源。 .NET中的每個託管線程都需要爲該堆棧分配1 MB的內存。對於持續5秒製作IO的網頁,每秒加載500次請求的情況下,您的線程池中需要2,500個線程,這意味着線程堆棧的內存爲2.5 GB,無需任何操作。然後你就會遇到上下文切換的問題,這會嚴重影響你的機器的性能(影響機器上的所有服務,而不僅僅是你的Web應用程序)。儘管Windows在忽略等待線程方面做得相當不錯,但它並不是用來處理如此大量的線程。請記住,當運行的線程數量等於機器上的邏輯CPU數量(通常不超過16)時,可以獲得最高的效率。

因此增加線程池的大小是一個解決方案,而人們也一直在這樣做了十年(甚至在微軟自己的產品),它僅僅是少可擴展性和高效率,在內存和CPU方面使用情況,而且你總是處於可能導致飢餓的IO延遲突然增加的情況下。直到C#5.0之前,異步代碼的複雜性對許多人來說都是不值得的。 async/await會像現在一樣改變一切,您可以從異步IO的可伸縮性中受益,並且可以同時編寫簡單的代碼。

更多細節:http://msdn.microsoft.com/en-us/library/ff647787.aspx使用異步調用來調用Web服務或遠程對象時,有執行額外的並行處理,而Web服務調用進行可能的話,應避免同步(阻塞)的機會調用Web服務,因爲傳出的Web服務調用是通過使用ASP.NET線程池中的線程來完成的。阻塞調用減少了處理其他傳入請求的可用線程數。

+14

此回覆不回答問題的第二部分。 – nunespascal 2012-06-12 04:23:36

+1

爲什麼轉向異步模式的理由很合理。 – eduncan911 2012-11-03 00:47:48

+3

我不認爲這解決了這樣一個事實,即不管I/O是不可預測的,無論其他規定如何,用戶在獲得響應之前都必須等待一切完成。 http/web服務器本身可以處理更多負載的事實並不意味着它能夠完全處理請求。我沒有看到async如何解決這個問題,除了改變事物的分佈方式以及可能引入更昂貴的上下文切換。使用asyc api的 – nilskp 2012-11-19 20:50:30

29
  1. 異步/伺機不是基於線程;它基於異步處理。當您在ASP.NET中進行異步等待時,請求線程返回到線程池,因此有異步服務該線程池的線程,直到異步操作完成。由於請求開銷低於線程開銷,這意味着異步/等待可以比線程池更好地擴展。
  2. 請求具有未完成異步操作的計數。此計數由SynchronizationContext的ASP.NET實現管理。您可以在my MSDN article中閱讀有關SynchronizationContext的更多信息 - 它涵蓋了ASP.NET的SynchronizationContext如何工作以及await如何使用SynchronizationContext

ASP.NET異步處理,有可能之前異步/ AWAIT - 你可以使用異步頁面,並使用EAP組件,如WebClient(基於事件的異步編程是一種基於SynchronizationContext異步編程風格)。異步/等待也使用SynchronizationContext,但有一個很多語法。

+1

這對我來說還是有點難理解,但是感謝您的信息和您的文章。它澄清了一些事情:)你能解釋異步處理和線程之間的巨大差異嗎?我認爲,如果我執行了一些代碼,並等待它運行在不同的線程上,那麼當前線程可以返回到池中。 – 2012-02-26 14:38:57

+2

@WouterdeKort'async'使代碼異步運行,但不啓動一個新線程,就像它正在執行當前線程中的代碼一樣,但是'SynchronizationContext'將在異步代碼行和其餘的方法... – 2012-02-26 14:54:50

+1

@Wouter異步處理不需要線程。在ASP.NET中,如果您正在等待一個未完成的操作,那麼'await'會將該方法的其餘部分安排爲延續,然後返回。該線程將返回到線程池,而不會爲請求提供服務。之後,當await操作完成時,它將從線程池中取出一個線程並繼續爲該線程上的請求提供服務。所以,異步編程不依賴於線程。雖然如果你需要的話,它可以和線程一起工作:你可以使用Task.Run來「等待」線程池操作。 – 2012-02-27 04:07:17

6

想象的線程池爲一組,你已經僱用的工作的工人。你的工人跑得快CPU指令代碼。

現在你的工作情況依賴於另一個慢傢伙的工作;慢傢伙作爲網絡舉例來說,你的工作可以有兩個部分,即具有之前執行慢傢伙的工作的一部分,並具有以電子郵件的一部分。執行之後緩慢的傢伙的工作。

你會如何建議你的工人做你的工作?你會對每個工人說 - 「先做這個第一部分,然後等到那個慢慢的人完成了,然後做第二個部分」?你會增加你的工人數量嗎?因爲他們似乎都在等待那個緩慢的傢伙,而你無法滿足新客戶?沒有!

你會問每個工人做第一部分,並要求慢慢的人回來,並在完成後放下隊列中的消息。您會告訴每個工作人員(或者可能是專門的工作人員)在隊列中查找已完成的消息並執行第二部分工作。

智能內核您所指的是操作系統維護緩慢磁盤和網絡IO完成消息的隊列的能力。

+2

很好的解釋,真的爲我釘了,謝謝! – 2016-07-21 14:18:45

相關問題