任务可靠性(还是我应该做些其他事情?)

时间:2018-07-16 08:52:50

标签: c# task nancy

我有一个使用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()

希望我的这种经历可以帮助其他人。我还不确定上述更改(有很多这样的更改)是否可以防止线程饥饿。

2 个答案:

答案 0 :(得分:1)

任务失败时...也就是说,当内部发生异常时

{1,...n}

该异常将存储在返回的任务中。即add jar file:///root/XXX.jar。任务将被标记为错误。

除非您调用Task.Waitawait任务或类似任务,否则不会在您的上下文中引发此异常。有关我建议您使用的内容,请参见下文。


pollTask = Task.Run(async () => { while (processTask) { connectionPool.PollEvents(); await Task.Delay(configLoader.config.connectionPollDelay, wtoken.Token); } keepRunning = false; }, wtoken.Token); 最终可能会抛出一些异常,由于我上面提到的原因,您不会被抓住。

如果您无法阻止该异常,则可能要在任务中处理该异常……除非我们正在谈论的是一种可能会破坏Task.Exception或更严重的条件。

我不知道是什么原因导致connectionPool.PollEvents();跳闸。一种可能性是您在其他地方使用了相同的对象,并且它不是线程安全的。

谈到线程安全。我希望connectionPoolPollEventsvolatile。尽管,正如您将在下面看到的那样,您不需要它们。


关于长期运行的任务,请使用以下方法:

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 );