调用异步方法而不等待阻止ASP.NET核心服务的其余部分的执行



我目前正在开发ASP.NET Core WebApp,它由web服务器和两个长期运行的服务组成——TCP服务器(用于管理我自己的客户端(和TCP客户端

这两个服务都与web服务器一起运行——我通过使它们继承BackgroundService并以这种方式注入DI来实现这一点:

services.AddHostedService(provider => provider.GetService<TcpClientService>());
services.AddHostedService(provider => provider.GetService<TcpServerService>());

不幸的是,在开发过程中,我遇到了一个奇怪的问题(这让我晚上睡不着,所以现在我请求你的帮助(。由于某些原因,TcpClientService中的异步代码会阻止其他服务(web服务器和tcp服务器(的执行。

using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ClientService.AsyncPoblem
{
public class TcpClientService : BackgroundService
{
private readonly ILogger<TcpClientService> _logger;
private bool Connected { get; set; }
private TcpClient TcpClient { get; set; }
public TcpClientService(ILogger<TcpClientService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
if (Connected)
{
await Task.Delay(100, stoppingToken); // check every 100ms if still connected
}
else
{
TcpClient = new TcpClient("localhost", 1234);
HandleClient(TcpClient); // <-- Call causing the issue
_logger.Log(LogLevel.Debug, "After call");
}
}
catch (Exception e)
{
// log the exception, wait for 3s and try again
_logger.Log(LogLevel.Critical, "An error occured while trying to connect with server.");
_logger.Log(LogLevel.Critical, e.ToString());
await Task.Delay(3000, stoppingToken);
}
}
}
private async Task HandleClient(TcpClient client)
{
Connected = true;
await using var ns = client.GetStream();
using var streamReader = new StreamReader(ns);
var msgBuilder = new StringBuilder();
bool reading = false;
var buffer = new char[1024];
while (!streamReader.EndOfStream)
{
var res = await streamReader.ReadAsync(buffer, 0, 1024);
foreach (var value in buffer)
{
if (value == 'x02')
{
msgBuilder.Clear();
reading = true;
}
else if (value == 'x03')
{
reading = false;
if (msgBuilder.Length > 0)
{
Console.WriteLine(msgBuilder);
msgBuilder.Clear();
}
}
else if (value == 'x00')
{
break;
}
else if (reading)
{
msgBuilder.Append(value);
}
}
Array.Clear(buffer, 0, buffer.Length);
}
Connected = false;
}
}
}

导致问题的调用位于ExecuteAsync方法的else语句中

else
{
TcpClient = new TcpClient("localhost", 1234);
HandleClient(TcpClient); // <-- Call causing the issue
_logger.Log(LogLevel.Debug, "After call");
}

代码从套接字中正确读取,但它阻止了WebServer和TcpServer的初始化。实际上,甚至连log方法都没有达到。无论我是否将wait放在HandleClient((前面,代码的行为都是一样的。

我做了一些测试,发现这段代码不再阻塞("调用后"日志显示(:

else
{
TcpClient = new TcpClient("localhost", 1234);
await Task.Delay(1);
HandleClient(TcpClient); // <- moving Task.Delay into HandleClient also works
_logger.Log(LogLevel.Debug, "After call");
}

这也像一个符咒一样工作(如果我尝试等待Task.Run((,它将阻止";"呼叫后";日志,但应用程序的其余部分将毫无问题地启动(:

else
{
tcpClient = new TcpClient("localhost", 6969);
Connected = true;
Task.Run(() => ReceiveAsync(tcpClient));
_logger.Log(LogLevel.Debug, "After call");
}

还有几个组合使它起作用,但我的问题是——为什么其他方法起作用(尤其是1ms延迟——这完全关闭了我的大脑(,而在没有等待的情况下启动HandleClient((则不行?我知道火与遗忘可能不是最优雅的解决方案,但它应该起作用,做它的工作,不是吗?我找了将近一个月,仍然没有找到任何解释。在这一点上,我晚上很难入睡,因为我没有人可以问,也无法停止思考。。

更新

(很抱歉消失了一天多没有任何答案(

经过许多小时的调查,我再次开始调试。每次我在HandleClient((中点击while循环时,我都会失去对调试器的控制,程序似乎可以继续工作,但它永远不会到达等待streamReader.ReadAsync((。在某个时候,我决定将while循环中的条件更改为true(我不知道为什么以前没有想过尝试它(,一切都开始按预期进行。消息将从tcp套接字中读取,其他服务将在没有任何问题的情况下启动。

以下是导致问题的代码

while (!streamReader.EndOfStream) <----- issue
{
var res = await streamReader.ReadAsync(buffer, 0, 1024);
// ...

在那次观察之后,我决定在到达循环之前打印出EndOfStream的结果,看看会发生什么

Console.WriteLine(streamReader.EndOfStream);
while (!streamReader.EndOfStream)
{
var res = await streamReader.ReadAsync(buffer, 0, 1024);
// ...

现在,完全相同的事情正在发生,但在到达循环之前!

解释

注:我不是高级程序员,尤其是在处理异步TCP通信时,所以我可能错了,但我会尽力做到最好。

streamReader.EndOfStream不是一个常规字段,它是一个属性,它的getter内部有逻辑。

这就是从内部看的样子:

public bool EndOfStream
{
get
{
ThrowIfDisposed();
CheckAsyncTaskInProgress();
if (_charPos < _charLen)
{
return false;
}
// This may block on pipes!
int numRead = ReadBuffer();
return numRead == 0;
}
}

EndOfStream getter是同步方法。为了检测流是否已结束,它调用ReadBuffer((。由于缓冲区中还并没有数据,流也并没有结束,所以方法将挂起,直到有一些数据要读取。不幸的是,它不能在异步上下文中使用,它总是会阻塞(不幸的是因为它似乎是立即检测中断连接、断开电缆或流结束的唯一方法(。

我还没有完成这段代码,我需要重写它,并添加一些断开的连接检测。我会尽快发布我的解决方案。

我要感谢所有帮助我的人,尤其是@RoarS。他在讨论中发挥了最大的作用,并花了一些时间仔细研究我的问题。

这是BackgroundService类的记录不足的行为。所有已注册的IHostedService都将按照它们已注册的顺序依次启动。直到每个IHostedServiceStartAsync返回后,应用程序才会启动。BackgroundService是在从StartAsync返回之前启动ExecuteAsync任务的IHostedService。异步方法将运行到第一次调用,以等待未完成的任务,然后返回。

TLDR;如果您不在ExecuteAsync方法中等待任何东西,服务器将永远不会启动。

由于您没有等待异步方法,因此您的代码可以归结为:;

while(true)
HandleClient(...);

(你真的想以CPU的速度产生无限数量的TcpClient吗?(。有一个非常简单的解决方案;

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Yield();
// ...
}

相关内容

最新更新