我有一个使用Nancy和Nancy.Hosting.Self的C#控制台应用程序。
这个想法是它将通过Nancy服务于API,并且主应用程序将例行轮询与各种应用程序的连接,并在通过API请求时(通过Nancy)从这些连接中获取数据。
所以我将有2个正在运行的进程,即恒定轮询和HTTP服务器。
我的Program.cs包含以下片段。
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,它最终详细说明了“ Faulted”。但是我不知道为什么。我也质疑长期/永久运行的Task的可靠性。
为避免这种含糊不清,我有一个主要问题。 Task是否适合上述方式中永久运行的任务。如果不是,我应该使用什么来实现2个并行过程,其中之一是Nancy。
更新(17/07/2018):
接受了到目前为止的建议和答案之后,我已经能够确定最终会发生的异常并终止了该过程:
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
定义。 这可能是ThreadPool饥饿的原因吗?
更新2
通过删除包装了Task.Run
的{{1}},更正了上面的代码。然后,调用代码将调用PostAsync
,因此该方法现在看起来像:
Heartbeat().Wait()
希望我的这种经历可以帮助其他人。我还不确定上述更改(有很多这样的更改)是否可以防止线程饥饿。
答案 0 :(得分:1)
任务失败时...也就是说,当内部发生异常时
{1,...n}
该异常将存储在返回的任务中。即add jar file:///root/XXX.jar
。任务将被标记为错误。
除非您调用Task.Wait
,await
任务或类似任务,否则不会在您的上下文中引发此异常。有关我建议您使用的内容,请参见下文。
pollTask = Task.Run(async () => {
while (processTask) {
connectionPool.PollEvents();
await Task.Delay(configLoader.config.connectionPollDelay, wtoken.Token);
}
keepRunning = false;
}, wtoken.Token);
最终可能会抛出一些异常,由于我上面提到的原因,您不会被抓住。
如果您无法阻止该异常,则可能要在任务中处理该异常……除非我们正在谈论的是一种可能会破坏Task.Exception
或更严重的条件。
我不知道是什么原因导致connectionPool.PollEvents();
跳闸。一种可能性是您在其他地方使用了相同的对象,并且它不是线程安全的。
谈到线程安全。我希望connectionPool
和PollEvents
是volatile。尽管,正如您将在下面看到的那样,您不需要它们。
关于长期运行的任务,请使用以下方法:
processTask
或者Factory.StartNew
的任何TaskCreationOptions
重载。
在内部使用keepRunning
创建任务时,它将专用于Task.Factory.StartNew(mehtod, TaskCreationOptions.LongRunning);
而不是从线程池(preventing starvation)中窃取任务,并确保所有工作正常该设置。
附录:通过StartNew is dangerous,WBuck表示容易出错。除了启动任务之外,TaskCreationOptions.LongRunning
还将执行错误处理(我告诉您要这样做)并设置Thread
。
还有一个陷阱,因为它无法识别Task.Run
方法(传递TaskCreationOptions.DenyChildAttach
时重载不会async
,使用Func<Task<TResult>>
是一个陷阱)。
因此,直接在Func<TResult>
中使用异步方法是一个坏主意。当然,幼稚的方法是将异步方法包装在常规方法中:
Func<Task>
但是,这违背了目的,因为现在异步方法位于线程池中,并且我们创建了一个线程来等待它。
当我们将StartNew
方法传递给Task.Factory.StartNew
(
()=>
{
awyncMethod().Wait();
},
TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach
);
时发生的事情是,我们得到了一个代表您实际想要的任务的创建的任务。让我们称之为“ faketask”。这个faketask将立即完成,而您想要的任务就是结果……要正确使用它,您需要向faketask添加延续(使用适当的创建选项),以检索实际任务。另外,理想情况下,您希望这种延续可以充当实际任务的代理(为您提供返回的内容或抛出的异常)。值得庆幸的是,TaskExtensions.Unwrap
完成了所有工作。
因此,我们到了:
async
另请参阅Parallel Programming with .NET - Task.Run vs Task.Factory.StartNew。
如果您打算保留其中的多个,则全部取自相同的Factory.StartNew
...首先确保Task.Factory.StartNew
(
async ()=>
{
/*...*/
},
TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach
).Unwrap();
是线程安全的。除此之外,您还需要一种监视任务状态并重新启动它们的方法。使用另一个长期运行的任务,该任务将进入所有Task.WaitAny
中并处理异常-并进行记录-并重新启动它们,只要应用程序应保持活动状态即可。 这就是我建议您用来等待任务的东西。
另一件事,您可以使用connectionPool
退出循环,选中CancellationToken.IsCancellationRequested
。并且知道任务已停止,您可以检查Task.IsCompleted
。 这就是为什么您不需要那些变量。
附录:实际上,您可以使用一个connectionPool
将其全部撕毁。
答案 1 :(得分:0)
正如评论中指出的那样,您的try-catch
是无用的。您需要等待任务完成才能查看是否引发了异常。
作为建议,为什么不使用计时器定期轮询呢?
var pollTask = Task.Run(async () =>
{
while (processTask)
{
wToken.ThrowIfCancellationRequested();
connectionPool.PollEvents();
await Task.Delay(configLoader.config.connectionPollDelay, wtoken.Token);
}
keepRunning = false;
}, wtoken.Token);
try
{
pollTask.Wait(wToken);
}
catch( AggregateException ex )
{
// Handle exception
}
或者,如果您的方法被标记为异步,则可以等待任务。 Await将为您打开汇总异常。
try
{
await Task.Run(async () =>
{
while (processTask)
{
wToken.ThrowIfCancellationRequested();
connectionPool.PollEvents();
await Task.Delay(configLoader.config.connectionPollDelay, wtoken.Token);
}
keepRunning = false;
}, wtoken.Token);
}
catch( Exception ex )
{
// Handle exception
}
为了完整起见,您还可以连续检查任务的异常属性。
Task.Run( async ( ) =>
{
wToken.ThrowIfCancellationRequested( );
connectionPool.PollEvents( );
await Task.Delay( configLoader.config.connectionPollDelay, wToken );
}, wToken ).ContinueWith( task =>
{
if( task.IsFaulted )
{
// Inspect the exception property of the task
// to view the exception / exceptions.
Console.WriteLine( task.Exception?.InnerException );
}
}, wToken );