HttpClient.PostAsync挂起直到所有系统.计时器已启动

本文关键字:有系统 计时器 启动 PostAsync 挂起 HttpClient | 更新日期: 2023-09-27 18:16:38

我有一个奇怪的问题与HttpClient和定时器。我有大量的对象(多达10,000)发布到web服务。这些对象有定时器,并在创建后的某个时间发送到服务。问题是Post会停止,直到所有计时器都启动。请参阅下面的代码以获取示例。

问:为什么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();
}

HttpClient.PostAsync挂起直到所有系统.计时器已启动

因为你用光了所有的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属性是问题所在。当你使用它时,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 ...;
    });
}

当您运行在使用默认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转换为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请求和返回

这是因为计时器设置得非常快,所以它们在PostAsync完成时都完成了设置。试着在timer.Start之后放一个Thread.Sleep(1000),这会减慢定时器的设置,你应该会看到一些PostAsync执行完成。

顺便说一下,Task.Result是一个阻塞操作,在GUI应用程序中运行时可能会导致死锁。本文中有更多信息