【问题标题】:Understanding async behaviour of C# console TCP server了解 C# 控制台 TCP 服务器的异步行为
【发布时间】:2018-08-08 16:45:12
【问题描述】:

我想在 .NET Core 2.0 中使用 async/await 方法使用异步(因为据我了解,它比生成线程更合理)(因为我相信它比具有IAsyncResult*Begin/*End 方法的那个)。

我编写了这个小型服务器,它接受来自客户端的新连接,然后开始向它们发送 100 条消息(它们之间有 1 秒的延迟)。

主要问题是:

如果我没有产生新线程,那么服务器如何继续向多个客户端发送延迟消息,而实际上它是“等待连接”是否涉及任何隐藏的低级信号/事件,或者真的只是新线程?

第二个问题是:

如果我没有使用这个全新的 async Main 语法糖并且我没有“等待”发送消息的 async 任务——我是否正确使用了异步?

class Program
{
    public static void Main(string[] args)
    {
        StartServer();
    }

    public static void StartServer()
    {
        IPAddress localhost = IPAddress.Parse("127.0.0.1");
        TcpListener listener = new TcpListener(localhost, 5567);

        Console.WriteLine($"Starting listening on {listener.Server.LocalEndPoint}");
        listener.Start();
        while (true)
        {
            Console.WriteLine("Waiting for connection...");
            var client = listener.AcceptTcpClient(); // synchronous
            Console.WriteLine($"Connected with {client.Client.RemoteEndPoint}!");

            Console.WriteLine("Starting sending messages...");
            SendHundredMessages(client); // not awaited -- StartServer is not async
        }
    }


    public static async Task SendHundredMessages(TcpClient client)
    {
        var stream = client.GetStream();

        for (int i=0; i<100; i++)
        {
            var msg = Encoding.UTF8.GetBytes($"Message no #{i}\n");
            await stream.WriteAsync(msg, 0, msg.Length);    // returning back to caller?

            await Task.Delay(1000);     // and what about here?
        }
        client.Close();
    }
}

原代码和下面的版本有什么区别? async Main 有什么不同?

class Program
{
    public static async Task Main(string[] args)
    {
        await StartServer();
    }

    public static async Task StartServer()
    {
        IPAddress localhost = IPAddress.Parse("127.0.0.1");
        TcpListener listener = new TcpListener(localhost, 5567);

        Console.WriteLine($"Starting listening on {listener.Server.LocalEndPoint}");
        listener.Start();
        while (true)
        {
            Console.WriteLine("Waiting for connection...");
            var client = await listener.AcceptTcpClientAsync(); // does it make any difference when done asynchronously?
            Console.WriteLine($"Connected with {client.Client.RemoteEndPoint}!");

            Console.WriteLine("Starting sending messages...");
            SendHundredMessages(client); // cannot await here, because it blocks next connections
        }
    }


    public static async Task SendHundredMessages(TcpClient client)
    {
        var stream = client.GetStream();

        for (int i=0; i<100; i++)
        {
            var msg = Encoding.UTF8.GetBytes($"Message no #{i}\n");
            var result = stream.WriteAsync(msg, 0, msg.Length);

            await Task.Delay(1000);
            await result;
        }
        client.Close();
    }
}

【问题讨论】:

  • 为什么StartServer async Task 没有await 什么?你至少应该使用await listener.AcceptTcpClientAsync();
  • 因为我之前在async Main 中尝试过await StartServer() 并且(失败后)我忘记将这个方法的类型改回void。
  • 如果我 awaitAcceptTcpClientAsync() 没有将 void Main 更改为 async Task MainStartServer();await StartServer(); 则程序到此结束。另一方面——当调用者只是这个几乎是空的 Main 方法时,等待 Accept 的目的是什么?
  • 因为AcceptTcpClient 是一种I/O 有界方法(如stream.WriteAsync),因此是真正的异步方法,它从异步代码中获益最多。谁叫它并不重要。您应该在 Main 上阻止 StartServer 并将其保留为 async Task
  • @CamiloTerevinto 我编辑了原始代码(您提到的 StartServer 类型)。在您添加评论之前,我还添加了一个后续问题。如果服务器目前什么都不做,Accept 异步有什么好处?

标签: c# tcp server async-await .net-core


【解决方案1】:

主要问题的答案:

在后台,对象通常使用一些 api(windows 的 winapi)。这些 api 可以不同地实现异步。例如,事件(winapi 事件)或回调。所以答案是肯定的——存在隐藏的信号或线程。例如,您可以看到 Ping 类。 Ping.InternalSend 使用 ThreadPool.RegisterWaitForSingleObject 来完成异步任务。

关于 async Main 的答案:

在您的第一个代码中,因为 StartServer 不是异步的,所以 Main 方法在您的“接受”周期结束之前不会重新获得控制权。

在您的第二个代码中,Main 方法将重新获得控制权,然后调用 listener.AcceptTcpClientAsync。但是因为您使用await StartServer();,所以 Main 方法将等到StartServer 结束。

一些代码解释:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TestConsole
{
    class Program
    {
        static void Main(string[] args)
        {   
            // not await for exploration
            var task = StartServer();

            Console.WriteLine("Main StartServer finished");
            task.Wait();
            Console.WriteLine("task.Wait() finished");
            Console.ReadKey();
        }        

        static async Task StartServer()
        {
            Console.WriteLine("StartServer enter");

            for(int i = 0; i < 3; i++) {
                await Task.Delay(1000); // listener.AcceptTcpClientAsync simulation
                SendHundredMessages();
            }

            Console.WriteLine("StartServer exit");
        }

        static async Task SendHundredMessages()
        {
            Console.WriteLine("SendHundredMessages enter");

            await Task.Run(() => {
                Thread.Sleep(2000);
            });

            Console.WriteLine("SendHundredMessages exit");
        }        
    }
}

此代码生成此输出:

StartServer进入
主 StartServer 完成
SendHundredMessages 输入
SendHundredMessages 输入
SendHundredMessages 退出
SendHundredMessages 输入
启动服务器退出
task.Wait() 完成
SendHundredMessages 退出
SendHundredMessages 退出

如您所见,Main 方法在第一个 Task.Delay 之后继续执行。

警告:

你不等待SendHundredMessages 的结束,这很糟糕。在示例输出中,您可以看到“task.Wait() 完成”之后的“SendHundredMessages ending”。在示例应用中当然没有危险,但在实际项目中可能会遇到大问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-12-26
    • 1970-01-01
    • 1970-01-01
    • 2010-10-22
    • 1970-01-01
    • 2014-04-08
    • 2014-12-22
    相关资源
    最近更新 更多