HttpClient.PostAsync挂起直到所有System.Timers都启动

时间:2014-08-06 01:53:19

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

我遇到了HttpClient和Timers的奇怪问题。我有大量的对象(最多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();
}

4 个答案:

答案 0 :(得分:2)

因为您正在耗尽所有ThreadPool个帖子。

您的示例代码存在很多问题。你正在杀死任何合理表现的机会,更不用说整个事情本质上是不稳定的。

你在一个循环中创建了一千个计时器。你没有保留对它们中的任何一个的引用,所以它们将在GC下次运行时被收集 - 所以我希望在实践中,它们实际上很少会运行,除非它们实际分配的内存非常少跑去跑。

将在ThreadPool线程上调用计时器的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.您可能希望限制并发请求的数量或类似的内容,而不是以预定义的时间间隔运行请求(并忽略某些请求可能需要很长时间,超时或某些请求的事实)。

答案 1 :(得分:1)

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

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

答案 2 :(得分:0)

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

顺便说一下,Task.Result是一个阻塞操作,当从GUI应用程序运行时会导致死锁。此article.

中有更多信息

答案 3 :(得分:0)

当您在使用默认ThreadPoolSynchronizationContext的控制台应用程序上运行时,您不应该真正体验到&#34;挂起&#34;感觉好像你在UI应用程序中。我认为它是因为Post需要更长的时间才能返回,而不是分配1000个计时器。

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

让我们看看我们需要做些什么才能实现&#34;同步异步&#34;:

首先,让我们将Runvoid更改为async Task,以便我们await方法Post

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

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

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