我遇到了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();
}
答案 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;:
首先,让我们将Run
从void
更改为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请求并返回。