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();
}
因为你用光了所有的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
属性只会阻塞异步操作,直到它完成。
让我们看看我们需要做些什么来让它成为"全程异步":
首先,让我们将Run
从void
更改为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应用程序中运行时可能会导致死锁。本文中有更多信息