在async函数中使用async

本文关键字:async 函数 | 更新日期: 2023-09-27 18:10:28

我正在制作一个简单的服务器,侦听客户端,它将读取客户端请求,做一些计算,发送响应回客户端并再次关闭ASAP(有点类似于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()来代替吗?我很好奇这是否重要,因为我已经在一个异步函数中,它不会阻塞主线程。

我的最后一个问题是:

    这是最好的/正确的方式来完成这个任务(也通过使用ManualResetEvent类)?
  1. 在这种情况下,读写流时使用异步或非异步操作有什么区别吗?(因为我没有阻塞主线程)
  2. 如果它滞后,并且需要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
    }
}

在async函数中使用async

马上我就看到了两个常见的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.
});

这是最好的/正确的方式来完成这个任务(也通过使用ManualResetEvent类)?

。您启动一个异步操作,然后立即等待它。不知道为什么,我经常看到这种疯狂的舞蹈。只要让它同步:

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

那么简单。

在这种情况下,读写流时使用异步或非异步操作有什么区别吗?

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

如果你决定使用异步IO, ProcessClientAsync应该是一个async函数,就像你现在有它一样。

如果你决定同步IO,在一个新线程上启动ProcessClientAsync,以便能够同时处理多个客户端。

如果它滞后,并且需要1-2秒来发送/接收数据,在异步和非异步操作之间选择仍然重要吗?

只要独立处理单个客户机,就没有问题。同步和异步之间的选择只在大规模(同时打开数十个以上的连接)时起作用。

一个常见的错误是在不需要异步的情况下使事情过于复杂。基本上所有教程都会犯这个错误。