【发布时间】:2018-07-16 08:52:50
【问题描述】:
我有一个使用 Nancy 和 Nancy.Hosting.Self 的 C# 控制台应用程序。
这个想法是它将通过 Nancy 提供 API,并且主应用程序将定期轮询到各种应用程序的多个连接 + 在通过 API(通过 Nancy)请求时从这些连接中获取数据。
所以我将有 2 个正在运行的进程,持续轮询和 HTTP 服务器。
我的 Program.cs 包含以下 sn-ps。
Task pollTask = null;
try {
pollTask = Task.Run(async () => {
while (processTask) {
connectionPool.PollEvents();
await Task.Delay(configLoader.config.connectionPollDelay, wtoken.Token);
}
keepRunning = false;
}, wtoken.Token);
}
catch (AggregateException ex) {
Console.WriteLine(ex);
}
catch (System.Threading.Tasks.TaskCanceledException ex) {
Console.WriteLine("Task Cancelled");
Console.WriteLine(ex);
}
后来……
using (var host = new Nancy.Hosting.Self.NancyHost(hostConfigs, new Uri(serveUrl))) {
host.Start();
// ...
// routinely checking console for a keypress to quit which then sets
// processTask to false, which would stop the polling task, which
// in turn sets keepRunning to false which stops the application entirely.
}
轮询任务似乎只是死亡/停止,没有任何输出到控制台以指示它停止的原因。在检查控制台输入按键时,我还查询了 pollTask.Status,它最终详细说明了“故障”。但我不知道为什么。我也在质疑长期/永久运行的任务的可靠性。
为了防止这种含糊不清,我有一个主要问题。 Task 是否适合以上述方式永久运行的任务。如果不是,我应该使用什么来实现 2 个并行进程,其中一个是 Nancy。
更新(2018 年 7 月 17 日):
在采纳了迄今为止的建议和答案之后,我已经能够确定最终发生的异常并终止进程:
PollEvents process appears to be throwing an exception...
System.AggregateException: One or more errors occurred. ---> System.InvalidOperationException: There were not enough free threads in the ThreadPool to complete the operation.
at System.Net.HttpWebRequest.BeginGetRequestStream(AsyncCallback callback, Object state)
at System.Net.Http.HttpClientHandler.StartGettingRequestStream(RequestState state)
at System.Net.Http.HttpClientHandler.PrepareAndStartContentUpload(RequestState state)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ApiProxy.ServiceA.Connection.<>c__DisplayClass22_0.<<Heartbeat>b__0>d.MoveNext() in \api-proxy\src\ServiceA\Connection.cs:line 281
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
at ApiProxy.ServiceA.Connection.Heartbeat() in \api-proxy\src\ServiceA\Connection.cs:line 274
at ApiProxy.ServiceA.Connection.PollEvents(Nullable`1 sinceEventId) in \api-proxy\src\ServiceA\Connection.cs:line 313
at ApiProxy.ConnectionPool.PollEvents() in \api-proxy\src\ConnectionPool.cs:line 50
at ApiProxy.Program.<>c.<<Main>b__5_0>d.MoveNext() in \api-proxy\Program.cs:line 172
---> (Inner Exception #0) System.InvalidOperationException: There were not enough free threads in the ThreadPool to complete the operation.
at System.Net.HttpWebRequest.BeginGetRequestStream(AsyncCallback callback, Object state)
at System.Net.Http.HttpClientHandler.StartGettingRequestStream(RequestState state)
at System.Net.Http.HttpClientHandler.PrepareAndStartContentUpload(RequestState state)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ApiProxy.ServiceA.Connection.<>c__DisplayClass22_0.<<Heartbeat>b__0>d.MoveNext() in \api-proxy\src\ServiceA\Connection.cs:line 281<---
因为try...catch 现在位于任务中,这意味着虽然此错误反复发生并快速发生,但最终它似乎会自行纠正。然而,在要求更多线程池之前能够查询线程池的可用性将是理想的。看起来 ThreadPool 问题与我的代码无关,而是与 Nancy 无关。
查找错误的来源后,我发现它发生在以下情况:
public bool Heartbeat() {
if (connectionConfig.events.heartbeatUrl == "") {
return false;
}
var val = false;
var task = Task.Run(async () => {
var heartbeatRequest = new HeartbeatRequest();
heartbeatRequest.host = connectionConfig.host;
heartbeatRequest.name = connectionConfig.name;
heartbeatRequest.eventId = lastEventId;
var prettyJson = JToken.Parse(JsonConvert.SerializeObject(heartbeatRequest)).ToString(Formatting.Indented);
var response = await client.PostAsync(connectionConfig.events.heartbeatUrl, new StringContent(prettyJson, Encoding.UTF8, "application/json"));
// todo: create a heartbeatResponse extending a base response type
PingResponse heartbeatResponse = JsonConvert.DeserializeObject<PingResponse>(await response.Content.ReadAsStringAsync());
if (heartbeatResponse != null) {
Console.WriteLine("Heartbeat: " + heartbeatResponse.message);
val = heartbeatResponse.success;
}
else {
// todo: sentry?
}
});
task.Wait();
return val;
}
我将调用包装在 Task 中,因为否则我最终会得到到处都是 async 定义的海洋。 这是线程池饥饿的可能来源吗?
更新 2
通过删除包装PostAsync 的Task.Run 更正了上述代码。然后调用代码将调用Heartbeat().Wait(),因此该方法现在看起来像:
public async Task<bool> Heartbeat() {
if (connectionConfig.events.heartbeatUrl == "") {
return false;
}
var val = false;
var heartbeatRequest = new HeartbeatRequest();
heartbeatRequest.host = connectionConfig.host;
heartbeatRequest.name = connectionConfig.name;
heartbeatRequest.eventId = lastEventId;
var prettyJson = JToken.Parse(JsonConvert.SerializeObject(heartbeatRequest)).ToString(Formatting.Indented);
var response = await client.PostAsync(connectionConfig.events.heartbeatUrl, new StringContent(prettyJson, Encoding.UTF8, "application/json"));
PingResponse heartbeatResponse = JsonConvert.DeserializeObject<PingResponse>(await response.Content.ReadAsStringAsync());
if (heartbeatResponse != null) {
Console.WriteLine("Heartbeat: " + heartbeatResponse.message);
val = heartbeatResponse.success;
}
else {
// todo: sentry?
}
return val;
}
希望我的一些经验可以帮助其他人。我还不确定上述更改(有很多这样的更改)是否会防止线程饥饿。
【问题讨论】:
-
你的
try/catch在这种情况下是没用的:你正在生成一个新的Task,但不是awaiting(既不是同步的也不是异步的),所以有没有什么可抓到的。您应该将try/catch逻辑移动到任务内部或Wait()/await它所在的位置。