【问题标题】:HttpClient.PostAsync Hanging Until All System.Timers Are StartedHttpClient.PostAsync 挂起,直到所有 System.Timers 都启动
【发布时间】:2014-08-06 18:58:51
【问题描述】:

我在使用 HttpClient 和 Timers 时遇到了一个奇怪的问题。我有大量对象(最多 10,000 个)发布到 Web 服务。这些对象在计时器上,并在创建后的某个时间发布到服务。问题是 Post 会停止,直到所有计时器都启动。有关示例,请参见下面的代码。

问:为什么 Post 会挂起,直到所有 Timer 都开始?如何修复它以使帖子在其他计时器启动时正常运行?

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();
}

【问题讨论】:

    标签: c# async-await c#-5.0 dotnet-httpclient


    【解决方案1】:

    因为您正在用完所有 ThreadPool 线程。

    您的示例代码有很多问题。您正在扼杀任何获得合理性能的机会,更不用说整个事情本身就是不稳定的。

    您正在循环中创建一千个计时器。您没有保留对它们中的任何一个的引用,因此它们将在下次 GC 运行时被收集 - 所以我希望在实践中,它们中很少有人会真正运行,除非在它们实际分配之前分配的内存很少跑起来。

    计时器的Elapsed 事件将在线程池线程上调用。在该线程中,您同步等待一堆异步调用完成。这意味着您现在正在浪费一个线程池线程,并且完全浪费了异步方法的底层异步性。

    异步 ​​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 并满足您需要的限制。您可能希望限制并发请求的数量或类似的东西,而不是按预定义的时间间隔运行请求(并忽略某些请求可能需要很长时间、超时或类似的事实)。

    【讨论】:

    • 仅供参考:Task.Delay 还会在您每次调用时创建一个新计时器:referencesource.microsoft.com/#mscorlib/system/threading/Tasks/…
    • @Alxandr 你没抓住重点。一,它创建的计时器是有根的,所以在它过去之前不会被GC收集。第二,线程只需要启动异步 I/O 操作,而不是阻塞直到它完成。换句话说,即使它仍然是一堆计时器,它们实际上会相当有效地使用线程池。
    • 诺诺。我只是指出它仍然会创建 1000 个计时器,而您只需要 2 个。
    • @Alxandr 是的,这就是为什么我提到实现一个更好的定制调度程序会更聪明,而不是使用一千个计时器。那将超出答案的范围。当然,在这个特定的示例代码中(这很可能不是 OP 想要的),我们实际上只等待了一秒钟或一天,但我认为这只是 OP 在编码示例时犯的一个错误。我确实说过这是 still rather bad 解决方案,但它确实解决了 OP 面临的直接问题。
    【解决方案2】:

    正如另一个回复中提到的,Result 属性是问题所在。当您使用它时,asyc 将变为sync。如果您想在控制台或 Windows 服务应用程序中运行异步操作,请尝试 Nito AsyncEx 库。它创建了一个 AsyncContext。现在您可以将void Run 更改为Task Run,它可以等待并且不需要阻塞Result 属性,在这种情况下await Post 将在Run 方法中工作。

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

    【讨论】:

      【解决方案3】:

      当您在使用默认 ThreadPoolSynchronizationContext 的控制台应用程序上运行时,您不应该真的像在 UI 应用程序中那样体验“挂起”的感觉。我认为是因为Post 的返回时间比分配 1000 个计时器的时间要长。

      为了让您的方法运行async,它必须“一直异步”。如前所述,使用Task.Result 属性只会阻塞异步操作,直到它完成.

      让我们看看我们需要做什么才能“一直异步”:

      首先,让我们将Runvoid 更改为async Task,这样我们就可以在Post 方法上使用await

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

      现在,由于 Run 变得可等待,因为它返回 Task,我们可以将 Timer.Elapsed 转换为 async 事件处理程序和 await on Run

      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 请求并返回。

      【讨论】:

      • 我认为 async 事件处理程序应该只用于例如一个 winforms 应用程序,它们最终被使用。如果我正确理解了这个问题,这将导致在终结器中抛出 Tasks 上的任何未处理的异常(从而使您的整个应用程序崩溃)。
      • @Luaan 我不确定你为什么有这种理解。异步事件处理程序仅限于 UI 应用程序,并且async void 方法中的控制台和 UI 的异常处理是相同的。从 .NET 4.5 开始,终结器不会在未处理的异常上使进程崩溃,而是抛出 UnobservedTaskException
      • 好吧,这确实有道理。我在 4.0 中完成了大部分 Task 工作,因此我非常重视未观察到的异常(“手动”线程也是如此)。说实话,我认为允许这样的未观察到的异常并不是一个好主意......
      【解决方案4】:

      这是因为计时器设置得非常快,所以在PostAsync 完成时它们都已完成设置。尝试在timer.Start 之后添加Thread.Sleep(1000),这会减慢计时器的设置速度,您应该会看到一些PostAsync 执行完成。

      顺便说一句,Task.Result 是一个阻塞操作,当从 GUI 应用程序运行时会导致死锁。这个article.有更多信息

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-09-17
        • 1970-01-01
        • 2015-07-20
        • 2019-05-18
        • 1970-01-01
        • 2012-08-03
        • 1970-01-01
        • 2022-01-12
        相关资源
        最近更新 更多