![](/img/trans.png)
[英]HttpClient.PostAsync() method is hanging forever in C# xUnt test
[英]HttpClient.PostAsync Hanging Until All System.Timers Are Started
我在使用HttpClient和Timers時遇到了一個奇怪的問題。 我有大量對象(最多10,000個)發布到Web服務。 這些對象在計時器上,並在創建后的n個時間發布到服務。 問題在於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 ...;
});
}
這是因為計時器設置得非常快,所以在PostAsync
完成時它們都已完成設置。 嘗試將Thread.Sleep(1000)
timer.Start
之后,這會減慢計時器的設置,您應該會看到一些PostAsync
執行已完成。
順便說一句, Task.Result
是一個阻塞操作,當從GUI應用程序運行時,它可能導致死鎖。 本文中有更多信息。
當您在使用默認ThreadPoolSynchronizationContext的控制台應用程序上運行時,您實際上不應像在UI應用程序中那樣感到“掛”的感覺。 我認為這是因為Post
返回花費的時間比分配1000個計時器要長。
為了使您的方法運行async
,它必須一直“異步” 。使用Task.Result
屬性(如前所述)將簡單地阻止異步操作,直到完成為止。
讓我們看看我們要做到“一路異步”:
首先,將Run
從void
更改為async Task
以便我們可以await
Post
方法:
public async Task Run()
{
var result = await Post(someData);
DoOtherStuff();
}
現在,由於Run
等待,因為它返回Task
,所以我們可以將Timer.Elapsed
轉到async
事件處理程序,然后在Run
await
。
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請求,然后再返回。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.