簡體   English   中英

返回已完成任務的異步方法意外地變慢

[英]Async method returning a completed task unexpectedly slow

我有一些c#代碼可以在網絡服務器上正常運行。 該代碼使用async / await,因為它在生產環境中執行一些網絡調用。

我還需要對代碼進行一些模擬; 在模擬過程中,代碼會同時被調用數十億次。 模擬不執行任何網絡調用:使用模擬,使用Task.FromResult()返回值。 從模擬返回的值實際上模擬了可以在生產環境中接收的網絡調用的每個可能響應。

我發現使用async / await會產生一些開銷,但我也期望在返回已經完成的任務並且不應該有實際等待的情況下,性能應該沒有太大差異。

但是進行一些測試我注意到性能大幅下降(特別是在某些硬件上)。

我使用LinqPad測試了以下代碼,並打開了編譯器優化; 如果要在visual studio中直接測試,可以刪除.Dump()調用並將代碼粘貼到控制台應用程序中。

// SYNC VERSION

void Main()
{
    Enumerable.Range(0, 1_000_000_000)
        .AsParallel()
        .Aggregate(
            () => 0.0,
            (a, i) => Calc(a, i),
            (a1, a2) => a1 + a2,
            f => f
        )
        .Dump();
}

double Calc(double a, double i) => a + Math.Sin(i);

// ASYNC-AWAIT VERSION

void Main()
{
    Enumerable.Range(0, 1_000_000_000)
        .AsParallel()
        .Aggregate(
            () => 0.0,
            (a, i) => Calc(a, i).Result,
            (a1, a2) => a1 + a2,
            f => f
        )
        .Dump();
}


async Task<double> Calc(double a, double i) => a + Math.Sin(i);

async-await版本的代碼舉例說明了我的模擬代碼的情況。

我在i7機器上運行模擬非常成功。 但是當我嘗試在我們辦公室的AMD ThreadRipper機器上運行代碼時,我得到了一些非常糟糕的結果。

我在i7機器和AMD ThreadRipper上使用linq pad中的上述代碼運行了一些基准測試,結果如下:

TEST on i7 quad-core 3,67 Ghz (windows 10 pro x64):

sync version: 15 sec (100% CPU)
async-await version: 20 sec (93% CPU)
TEST on AMD 32 cores 3,00 Ghz (windows server 2019 x64):

sync version: 16 sec (50% CPU)
async-await version: 140 sec (14% CPU)

我知道存在硬件差異(可能是英特爾超線程更好等),但這個問題與硬件性能無關。

為什么不總是100%的CPU使用率(或50%考慮到CPU超線程的最壞情況),但async-await版本的代碼中的CPU使用率有所下降?

(AMD的CPU使用率下降更為明顯,但它也出現在英特爾上)

是否有任何解決方法不涉及在代碼周圍重構所有async-await調用鏈? (代碼庫大而復雜)

謝謝。

編輯

正如評論中所建議的,我試圖使用TaskTask insted of Task,它似乎解決了這個問題。 我在VS中直接嘗試了這個,因為我需要一個nuget包(Release build),這些是結果:

TEST on i7

"sync" version: 16 sec (100% CPU)
"await Task" version: 49 sec (95% CPU)
"await ValueTask" version: 31 sec (100% CPU)

TEST on AMD

"sync" version: 15 sec (50% CPU)
"await Task" version: 125 sec (12% CPU)
"await ValueTask" version: 17 sec (50% CPU)

老實說,我對ValueTask課程知之甚少,我將研究它。 如果你能解釋/詳細說明答案,歡迎你。

謝謝。

您的垃圾收集器很可能配置為工作站模式(默認),它使用單個線程來回收未使用對象分配的內存。 對於具有32個內核的機器,一個內核肯定不足以清理其余31個內核不斷產生的混亂! 所以你應該切換到服務器模式

<configuration>
  <runtime>
    <gcServer enabled="true"></gcServer>
  </runtime>
</configuration>

后台服務器垃圾收集使用多個線程,通常是每個邏輯處理器的專用線程。

通過使用ValueTask而不是Task ,可以避免堆中的內存分配,因為ValueTask是一個在堆棧中分配的結構,不需要垃圾回收。 但只有當它包裝完成任務的結果時才會出現這種情況。 如果它包含一個不完整的任務,那么它沒有任何優勢。 它適用於需要await數千萬個任務的情況,並且您希望絕大多數任務都能完成。

我想解決這個問題:

async-await版本的代碼舉例說明了我的生產代碼的情況。

你說你的生產版本“執行一些網絡電話”。 如果是這種情況,那么您在此處顯示的代碼不會舉例說明您的生產代碼。 Lasse在評論中提到了原因:您的async方法沒有異步運行。 原因在於如何await

await關鍵字查看您正在調用的方法返回的Task 您知道它將暫停該方法的執行並將該方法的其余部分注冊為Task的延續。 但你可能不知道的是, 只有在Task尚未完成時才會發生這種情況 如果在await查看Task時該Task已經完成,那么您的代碼將同步進行。 實際上,您應該看到編譯器警告告訴您:

CS1998:這種異步方法缺少'await'運算符並將同步運行。 考慮使用'await'運算符等待非阻塞API調用,或'await Task.Run(...)'在后台線程上執行CPU綁定工作。

因此,兩個代碼塊之間的唯一區別是async版本只會增加await的不必要開銷,以便仍然同步運行。

要擁有一個真正的異步方法,您實際上必須做一些需要等待的事情。 如果要模擬此,可以使用Task.Delay 即使你使用了最小的延遲( Task.Delay(TimeSpan.FromTicks(1)) ),它仍會觸發await它的工作。

async Task<double> Calc(double a, double i)
{
    await Task.Delay(TimeSpan.FromTicks(1));
    return a + Math.Sin(i);
}

當然,這會引入您之前沒有的延遲,因此您應該將它與使用Thread.Sleep同步持續時間的同步版本進行比較:

double Calc(double a, double i)
{
    Thread.Sleep(TimeSpan.FromTicks(1));
    return a + Math.Sin(i);
}

在我的Intel Core i7上,異步版本運行約22秒,同步版本運行約50秒。

通常我會說的異步代碼的所有優點被拋出當您使用的窗口.Result ,但使用的是AsParallel() ...但我仍然不知道如何會影響性能。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM