在异步函数中使用async?

3
我正在制作一个简单的服务器,它监听客户端并读取客户端请求,进行一些计算,然后向客户端发送响应并立即关闭连接(类似于HTTP)。
可能会有每秒钟出现很多连接,因此我希望让它尽可能快速和高效。
到目前为止,我能想到的最好方法如下所示:
private static ManualResetEvent gate = new ManualResetEvent(false);

static async void ListenToClient(TcpListener listener)
{
    Console.WriteLine("Waiting for connection");
    TcpClient client = await listener.AcceptTcpClientAsync();
    Console.WriteLine("Connection accepted & establised");
    gate.Set(); //Unblocks the mainthread

    Stream stream = client.GetStream();
    byte[] requestBuffer = new byte[1024];
    int size = await stream.ReadAsync(requestBuffer, 0, requestBuffer.Length);

    //PSEUDO CODE: Do some calculations

    byte[] responseBuffer = Encoding.ASCII.GetBytes("Ok");
    await stream.WriteAsync(responseBuffer, 0, responseBuffer.Length);

    stream.Close();
    client.Close();
}
static void Main(string[] args)
{
    TcpListener listener = new TcpListener(IPAddress.Any, 8888);
    listener.Start();
    while (true)
    {
        gate.Reset(); 
        ListenToClient(listener);
        gate.WaitOne(); //Blocks the main thread and waits until the gate.Set() is called
    }
}

注意:为了简化示例,我没有添加任何错误处理,例如try-catch,并且我知道这里的响应将始终为“Ok”。
这里的代码只是等待连接,当它到达await listener.AcceptTcpClientAsync()时,它会跳回while循环并等待直到建立连接并调用gate.Set(),以便它可以再次监听新连接。因此,这将允许多个客户端同时连接(特别是如果计算需要很长时间)。
但是我应该使用stream.ReadAsync()还是stream.Read()?我很好奇是否重要,因为我已经在一个异步函数中,它不会阻塞主线程。
所以我的最终问题是:
1. 这是实现此任务的最佳/正确方法吗(也通过使用ManualResetEvent类)? 2. 在读写流时使用异步或非异步操作是否会有任何差异?(因为我没有阻塞主线程) 3. 如果它延迟,并且需要1-2秒才能发送/接收数据,是否仍然重要选择异步和非异步操作?
更新新改进的内容如下:
private static ManualResetEvent gate = new ManualResetEvent(false);

static async Task ListenToClient(TcpListener listener)
{
    //Same Code
}
static void Main(string[] args)
{
    TcpListener listener = new TcpListener(IPAddress.Any, 8888);
    listener.Start();
    while (true)
    {
        gate.Reset();
        Task task = ListenToClient(listener);
        task.ContinueWith((Task paramTask) =>
            {
                //Inspect the paramTask
            });
        gate.WaitOne(); //Blocks the main thread and waits until the gate.Set() is called
    }
}

这里有一个很好的例子:https://dev59.com/5WEi5IYBdhLWcg3wueN0#21018042 - Yuval Itzchakov
2个回答

3

一开始我就看到了两个常见的async错误:

async void

不要这样做。编译器支持async void只是为了处理现有的事件驱动接口。但该方法并非如此,因此在这里它是一种反模式。使用async void会导致失去任何响应该任务或对其进行任何操作的方式,例如处理错误。

说到响应任务...

ListenToClient(listener);

您正在生成一个任务,但从未检查其状态。如果该任务中发生异常,您会怎么做?如果没有捕获到异常,它将被默默地忽略。至少应该为任务提供顶层回调函数,一旦任务完成,就可以调用该回调函数。即使只是像这样简单的代码:

ListenToClient(listener).ContinueWith(t =>
{
    // t is the task.  Examine it for errors, cancelations, etc.
    // Respond to error conditions here.
});

嘿,谢谢你的回答!但是你确定ContinueWith()不是遗留的东西吗?这个YouTube视频展示了它:https://www.youtube.com/watch?v=MCW_eJA2FeY&index=3&list=PLbjPvLkw5f21ji20qbzxfOL4Zy0LzkgTW 在55:30处,他说新的async await特性将把await后面的内容转换为Task.ContinueWith()。 - Assassinbeast
@Assassinbeast:仅仅因为某些东西出现在语言的早期版本中并不意味着它是“遗留的”。它仍然是有用的。请注意,您同步调用了ListenToClient()而没有使用await关键字。问题的第一步是ListenToClient()没有返回一个Task,您应该修复它。一旦这个问题被解决,您需要对这个任务做一些事情,目前您还没有。如果调用async方法的方法本身不是async,那么您不能使用await,您需要显式地处理Task - David
好的,我已经更新了它以返回一个任务,现在我可以检查它。这是你想要的吗?如果是这样,那么如果我不关心检查任务的状态怎么办?例如,如果它抛出异常,那么我就不在乎(因为我不知道该怎么做)...所以客户端应该能够处理错误,如果它没有收到消息的话。这样,服务器就可以继续工作而不会受到任何干扰。 - Assassinbeast
@Assassinbeast:在某些情况下,你可能没有任何有意义的方式来响应错误。但作为模式和实践的问题,你至少应该能够做出响应。在这种情况下,客户端可能会处理错误,也可能不会。或者可能会出现意外错误,你需要调试,但如果你忽略了所有错误,则调试将变得很棘手。至少,将它们发送到某种类型的日志记录机制中。即使该机制被配置为不在任何地方持久化日志事件。 - David
好的 :-) 但是我在问题中做的新更新是你想要我编辑的方式吗? - Assassinbeast
1
@Assassinbeast:那应该可以工作。您还可以通过抛出异常并查看在哪里/如何捕获它们并对其进行响应来测试事物。即使在此代码中您不一定想要,但总体模式很重要。 - David

2

这是实现此任务的最佳/正确方法吗(还可以使用ManualResetEvent类)?

不是。您启动异步操作,然后立即等待它。由于某种原因,我经常看到这种疯狂的舞蹈。只需将其变成同步:

while (true) {
 var clientSocket = Accept();
 ProcessClientAsync(clientSocket);
}

如此简单。

在读写流时,使用异步或非异步操作是否会有任何区别?

如果您有很多客户端,则使用套接字的异步IO很好。对于同时处理几十个客户端,您可以使用带线程的同步IO。异步IO是关于不阻止线程(每个线程使用1MB的堆栈空间)。

如果您决定使用异步IO,则ProcessClientAsync应该像现在一样是一个异步函数。

如果您选择同步IO,则启动ProcessClientAsync在新线程上以能够同时处理多个客户端。

如果它延迟并需要1-2秒才能发送/接收数据,是否仍然需要选择异步和非异步操作?

只要您独立地处理各个客户端,就可以了。同步和异步之间的选择仅在高规模(同时打开超过数十个连接)时才起作用。

过度复杂化事物而没有必要去异步化是常见的错误。基本上所有教程都犯了这个错误。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接