2014-01-28 67 views
40

我在ASP.NET應用程序中有一個方法,它需要花費很多時間才能完成。根據用戶提供的緩存狀態和參數,在一次用戶請求期間,對此方法的調用最多可能發生3次。每次通話大約需要1-2秒鐘才能完成。該方法本身是對服務的同步調用,並且不可能覆蓋實現。
所以給服務的同步調用看起來像下面這樣:將異步代碼包裝爲異步調用

public OutputModel Calculate(InputModel input) 
{ 
    // do some stuff 
    return Service.LongRunningCall(input); 
} 

,並且該方法的使用量(注意,方法是調用可能發生一次以上):

private void MakeRequest() 
{ 
    // a lot of other stuff: preparing requests, sending/processing other requests, etc. 
    var myOutput = Calculate(myInput); 
    // stuff again 
} 

我試圖從我身邊改變實現來提供這種方法的同時工作,這是我到目前爲止所做的。

public async Task<OutputModel> CalculateAsync(InputModel input) 
{ 
    return await Task.Run(() => 
    { 
     return Calculate(input); 
    }); 
} 

利用率(「做其他的東西」碼部分與呼叫服務同時運行):

private async Task MakeRequest() 
{ 
    // do some stuff 
    var task = CalculateAsync(myInput); 
    // do other stuff 
    var myOutput = await task; 
    // some more stuff 
} 

我的問題如下。我是否使用正確的方法來加快ASP.NET應用程序的執行速度,還是我在做不必要的工作來試圖異步運行同步代碼?任何人都可以解釋爲什麼第二種方法不是ASP.NET中的選項(如果它不是)?另外,如果這種方法適用,我是否需要異步調用這種方法,如果它是我們目前可能執行的唯一調用(我有這種情況,在等待完成時沒有其他任何東西需要執行)?
關於此主題的網絡中的大多數文章都包含使用async-await方法的代碼,該方法已經提供了awaitable方法,但這不是我的情況。 Here是描述我的情況的好文章,它沒有描述並行調用的情況,拒絕包裝同步調用的選項,但在我看來,我的情況恰恰是這樣做的時機。
在此先感謝您的幫助和提示。

回答

50

區分兩種不同類型的併發是很重要的。 異步併發性是指您在運行中有多個異步操作(並且由於每個操作都是異步的,因此它們都沒有實際使用線程)。 並行併發性是指每個執行單獨操作的多個線程。

要做的第一件事是重新評估這一假設:

的方法本身就是對服務的同步調用,有沒有可能重寫實現。

如果你的「服務」是一個網絡服務或其他任何爲I/O密集型的,那麼最好的解決辦法是寫一個異步API它。

我將繼續假設您的「服務」是必須在與Web服務器相同的機器上執行的CPU綁定操作。

如果是這樣的話,那麼接下來的事情是要評估一個假設:

我需要請求執行得更快。

你確定這是你需要做的嗎?是否有任何前端更改可以實現 - 例如,啓動請求並允許用戶在處理過程中執行其他工作?

我會繼續假設是的,您確實需要使單個請求更快地執行。

在這種情況下,您需要在Web服務器上執行並行代碼。這是絕對不推薦的,因爲並行代碼將使用ASP.NET可能需要處理其他請求的線程,並且通過刪除/添加線程,它將拋出ASP.NET線程池啓發式。所以,這個決定對整個服務器都有影響。

當您在ASP.NET上使用並行代碼時,您決定真正限制Web應用程序的可伸縮性。您還可能會看到相當數量的線程流失,尤其是在您的請求突然崩潰的情況下。如果您的知道併發用戶的數量很低(即不是公用服務器),我建議只在ASP.NET上使用並行代碼。

所以,如果你得到這麼多,你確定你想要在ASP.NET上進行並行處理,那麼你有幾個選項。

其中一種更簡單的方法是使用Task.Run,與您現有的代碼非常相似。但是,我不建議實施CalculateAsync方法,因爲這意味着處理是異步的(它不是)。相反,使用Task.Run在調用點:

private async Task MakeRequest() 
{ 
    // do some stuff 
    var task = Task.Run(() => Calculate(myInput)); 
    // do other stuff 
    var myOutput = await task; 
    // some more stuff 
} 

另外,如果你的代碼工作得很好,你可以使用Parallel類型,即Parallel.ForParallel.ForEachParallel.Invoke。到Parallel代碼的優點是,所述請求線程被用作在並行線程中的一個,然後在線程上下文恢復執行(有比async例如上下文切換以內):

private void MakeRequest() 
{ 
    Parallel.Invoke(() => Calculate(myInput1), 
    () => Calculate(myInput2), 
    () => Calculate(myInput3)); 
} 

我不推薦在ASP.NET上使用並行LINQ(PLINQ)。

+0

非常感謝您的詳細解答。還有一件事我想澄清。當我使用'Task.Run'開始執行時,內容會在另一個線程中運行,這是從線程池中獲取的。然後,根本不需要將阻塞調用(例如我的調用服務)封裝到「運行」中,因爲它們總是會消耗一個線程,並在執行該方法期間被阻塞。在這種情況下,在我的情況下,'async-await'留下的唯一好處是一次執行多個操作。請糾正我,如果我錯了。 – Eadel

+0

另外,僅僅爲了指定,「服務」是對遠程web服務的調用,它不是CPU綁定到我們的服務器,但它需要一些時間來獲得來自遠程機器的響應。關於第二個假設的話題,我們提供用戶在大多數請求期間做一些工作的選項,但在我們的情況下,速度和服務器負載都很重要。該應用程序不是內部應用程序,因此可能會有大量的請求高峯負載。 – Eadel

+0

是的,您從'Run'(或'Parallel')獲得的唯一好處是併發性。這些操作仍然會阻塞一個線程。既然你說服務是一個Web服務,那麼我不建議使用'Run'或'Parallel';相反,爲服務編寫一個異步API。 –