簡體   English   中英

HttpClient.PostAsync掛起,直到啟動所有System.Timers

[英]HttpClient.PostAsync Hanging Until All System.Timers Are Started

我在使用HttpClient和Timers時遇到了一個奇怪的問題。 我有大量對象(最多10,000個)發布到Web服務。 這些對象在計時器上,並在創建后的n個時間發布到服務。 問題在於Post停滯不前,直到所有計時器都已啟動。 有關示例,請參見下面的代碼。

問:為什么在所有計時器啟動之前,帖子會一直掛起? 如何解決該問題,以便在其他計時器啟動時帖子能夠正確運行?

public class MyObject
{
    public void Run()
    {
        var result = Post(someData).Result; 
        DoOtherStuff();
    }
}

static async Task<string> Post(string data)
{
    using (var client = new HttpClient())
    {
        //Hangs here until all timers are started
        var response = await client.PostAsync(new Uri(url), data).ConfigureAwait(continueOnCapturedContext: false);
        var text = await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false);

        return text;
    }
}

static void Main(string[] args)
{
    for (int i = 0; i < 1000; i++)
    {
        TimeSpan delay = TimeSpan.FromSeconds(1);
        if (i % 2 == 0) delay = TimeSpan.FromDays(1);

        System.Timers.Timer timer = new System.Timers.Timer();
        timer.AutoReset = false;
        timer.Interval = delay.TotalMilliseconds;
        timer.Elapsed += (x, y) =>
            {
                MyObject o = new MyObject();
                o.Run();
            };

        timer.Start();
    }

    Console.ReadKey();
}

因為您用完了所有ThreadPool線程。

您的示例代碼有很多錯誤。 您正在扼殺擁有合理性能的任何機會,更不用說整個過程本質上是不穩定的。

您正在循環創建一千個計時器。 您沒有保留對它們的任何引用,因此它們將在下一次GC運行時被收集-因此我希望在實踐中,只有很少的內存會真正運行,除非在它們實際分配之前分配的內存很少開始運行。

計時器的Elapsed事件將在ThreadPool線程上調用。 在該線程中,您同步等待一堆異步調用完成。 這意味着您現在正在浪費線程池線程,並且完全浪費了異步方法的底層異步性。

異步I / O的延續也將發布到ThreadPool-但是,ThreadPool充滿了計時器回調。 它會慢慢開始創建越來越多的線程來適應計划的工作量,直到最終它能夠執行來自異步I / O的第一個回調,並逐漸使自身糾結。 此時,您可能有1000多個線程,並且顯示出對如何進行異步編程的完全誤解。

解決這兩個問題的一種方法(仍然很糟糕)是始終僅使用異步性:

public class MyObject
{
    public async Task Run()
    {
        var result = await Post(someData);
        DoOtherStuff();
    }
}

static async Task<string> Post(string data)
{
    using (var client = new HttpClient())
    {
        //Hangs here until all timers are started
        var response = await client.PostAsync(new Uri(url), new StringContent(data)).ConfigureAwait(continueOnCapturedContext: false);
        var text = await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false);

        return text;
    }
}

static void Main(string[] args)
{
    var tasks = new List<Task>();

    for (int i = 0; i < 1000; i++)
    {
        TimeSpan delay = TimeSpan.FromSeconds(1);
        if (i % 2 == 0) delay = TimeSpan.FromDays(1);

        tasks.Add(Task.Delay(delay).ContinueWith((_) => new MyObject().Run()));
    }

    Task.WaitAll(tasks.ToArray());

    Console.WriteLine("Work done");
    Console.ReadKey();
}

更好的方法是實現一些調度程序,該調度程序處理需要分配的異步I / O。 您可能希望限制並發請求的數量或類似的數量,而不是按預定義的時間間隔運行請求(並且忽略某些請求可能會花費很長時間,超時或類似的事實)。

如另一個答復中所述,該Result屬性就是問題所在。 當您使用它時, asycsync 如果要在控制台或Windows服務應用程序中運行異步操作,請嘗試Nito AsyncEx庫。 它創建一個AsyncContext。 現在,您可以將void Run更改為Task Run ,它可以等待並且不需要阻塞Result屬性,在這種情況下, await Post將在Run方法中工作。

static void Main(string[] args)
{
    AsyncContext.Run(async () =>
    {
            var data = await ...;
    });
}

這是因為計時器設置得非常快,所以在PostAsync完成時它們都已完成設置。 嘗試將Thread.Sleep(1000) timer.Start之后,這會減慢計時器的設置,您應該會看到一些PostAsync執行已完成。

順便說一句, Task.Result是一個阻塞操作,當從GUI應用程序運行時,它可能導致死鎖。 本文中有更多信息

當您在使用默認ThreadPoolSynchronizationContext的控制台應用程序上運行時,您實際上不應像在UI應用程序中那樣感到“掛”的感覺。 我認為這是因為Post返回花費的時間比分配1000個計時器要長。

為了使您的方法運行async ,它必須一直“異步” 。使用Task.Result屬性(如前所述)將簡單地阻止異步操作,直到完成為止。

讓我們看看我們要做到“一路異步”:

首先,將Runvoid更改為async Task以便我們可以await Post方法:

public async Task Run()
{
    var result = await Post(someData);
    DoOtherStuff();
}

現在,由於Run等待,因為它返回Task ,所以我們可以將Timer.Elapsed轉到async事件處理程序,然后在Run await

static void Main(string[] args)
{
   for (int i = 0; i < 1000; i++)
   {
       TimeSpan delay = TimeSpan.FromSeconds(1);
       if (i % 2 == 0) delay = TimeSpan.FromDays(1);

       System.Timers.Timer timer = new System.Timers.Timer();
       timer.AutoReset = false;
       timer.Interval = delay.TotalMilliseconds;
       timer.Elapsed += async (x, y) =>
       {
            MyObject o = new MyObject();
            await o.Run();
       };

        timer.Start();
   }

   Console.ReadKey();
}

就是這樣,現在我們一直異步傳輸到HTTP請求,然后再返回。

暫無
暫無

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

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