确保调用Socket.XXXAsync的线程保持活动状态以完成IO请求(IO完成端口,C#)

时间:2011-03-28 22:31:18

标签: c# .net multithreading sockets asynchronous

我正在实施一个小型库,以便更轻松地使用System.Net.Sockets.Socket。它应该处理任意数量的侦听和接收TCP套接字,重要的是实现快〜。

我使用XXXAsync方法和CLR ThreadPool回调到库用户的代理(例如,每当成功发送消息或收到某些数据时)。优选的是库本身不会启动任何线程。

库的用户可以访问我的Sockets包装器的接口以发送消息或开始接收消息(在许多其他方法和重载中):

public interface IClientSocket {
    // will eventually call Socket.SendAsync
    IMessageHandle SendAsync(byte[] buffer, int offset, int length);

    // will eventually call Socket.RecieveAsync
    void StartReceiveAsync();
}

XXXAsync方法使用IO完成端口。因此,调用这些方法的线程必须保持活动状态,直到操作完成,否则操作将失败并显示SocketError.OperationAborted(我认为是这种情况,或者不是?)。 对库的用户施加这样的限制是丑陋且容易出错的。

这里最好的选择是什么?

  • 使用委托调用ThreadPool.QueueUserWorkItem来调用XXXAsync方法?这样安全吗?我在某处读到,ThreadPool不会停止有任何IOCP的空闲线程。这会很好,因为它解决了上面的问题 但是对于许多TCP连接来说也可能是坏事。 在这种情况下,每个ThreadPool线程可能会调用一个挂起的ReceiveAsync调用。因此,即使当前工作负载较低且许多线程处于空闲状态(并浪费内存),ThreadPool也不会收缩。

  • 启动一个始终处于活动状态的专用线程并调用XXXAsync方法。例如,当库用户想要发送数据时,它会将一个委托放入同步队列中,该线程会弹出它并调用SendAsync方法。
    我不太喜欢这个解决方案,因为它浪费了一个线程并且在多核机器上,发送只由一个线程执行。

此外,两种解决方案都没有最好的接缝,因为它们通过作业将异步方法调用到另一个线程。可以避免吗?

你怎么看? (谢谢!!)

托马斯

编辑1:

我可以使用以下测试程序重现SocketError.OperationAborted问题(我认为这是正确的) 编译,启动和telnet到端口127.0.0.1:10000。发送" t"或者" T"并等待> 3秒。发送" T"时,ReceiveAsync调用是在ThreadPool(工作)中完成的,其中" t"启动一个新线程,在3秒后终止(失败)。

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Collections.Generic;

namespace Test {
    class Program {
        static List<Socket> _referenceToSockets = new List<Socket>();
        static void Main(string[] args) {
            Thread.CurrentThread.Name = "Main Thread";

            // Create a listening socket on Port 10000.
            Socket ServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            _referenceToSockets.Add(ServerSocket);
            var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 10000);
            ServerSocket.Bind(endPoint);
            ServerSocket.Listen(50);

            // Start listening.
            var saeaAccept = new SocketAsyncEventArgs();
            saeaAccept.Completed += OnCompleted;
            ServerSocket.AcceptAsync(saeaAccept);

            Console.WriteLine(String.Format("Listening on {0}.", endPoint));
            Console.ReadLine();
        }

        private static void OnCompleted(object obj, SocketAsyncEventArgs evt) {
            var socket = (Socket)obj;
            Console.WriteLine(String.Format("Async operation completed: {0}; Error: {1}; Callback-Thread: \"{2}\" ({3} threadpool)", evt.LastOperation, evt.SocketError, Thread.CurrentThread.Name, Thread.CurrentThread.IsThreadPoolThread?"is":"no"));
            switch (evt.LastOperation) {
                case SocketAsyncOperation.Accept:
                    // Client connected. Listen for more.
                    Socket clientSocket = evt.AcceptSocket;
                    _referenceToSockets.Add(clientSocket);
                    evt.AcceptSocket = null;
                    socket.AcceptAsync(evt);

                    // Start receiving data.
                    var saeaReceive = new SocketAsyncEventArgs();
                    saeaReceive.Completed += OnCompleted;
                    saeaReceive.SetBuffer(new byte[1024], 0, 1024);
                    clientSocket.ReceiveAsync(saeaReceive);
                    break;
                case SocketAsyncOperation.Disconnect:
                    socket.Close();
                    evt.Dispose();
                    break;
                case SocketAsyncOperation.Receive:
                    if (evt.SocketError != SocketError.Success) {
                        socket.DisconnectAsync(evt);
                        return;
                    }
                    var asText = Encoding.ASCII.GetString(evt.Buffer, evt.Offset, evt.BytesTransferred);
                    Console.WriteLine(String.Format("Received: {0} bytes: \"{1}\"", evt.BytesTransferred, asText));
                    if (evt.BytesTransferred == 0) {
                        socket.Close();
                        evt.Dispose();
                    }
                    if (asText.ToUpper().StartsWith("T")) {
                        Action<object> action = (object o) => {
                            socket.ReceiveAsync(evt);
                            Console.WriteLine(String.Format("Called ReceiveAsync {0}...", o));
                            Thread.Sleep(3000);
                            Console.WriteLine("End of Action...");
                        };
                        if (asText.StartsWith("T")) {
                            ThreadPool.QueueUserWorkItem(o=>action(o), "in ThreadPool");
                        } else {
                            new Thread(o=>action(o)).Start("in a new Thread");
                        }
                    } else {
                        socket.ReceiveAsync(evt);
                    }
                    break;
            }
        }
    }
}

编辑#3

以下是我打算使用的解决方案:

我让库用户的线程直接调用XXXAsyncSendAsync除外)操作。在大多数情况下,调用将是成功的(因为调用线程很少终止) 如果操作失败并显示SocketError.OperationAborted,则库仅使用异步回调中的当前线程再次调用该操作。这个是一个ThreadPool线程,它有很好的成功机会(如果SocketError.OperationAborted的原因是由于某些其他错误导致的原因,将设置一个额外的标志,最多使用此解决方法一次) 。 这应该工作,因为套接字本身仍然正常,只是前一个操作失败。

对于SendAsync,此解决方法不起作用,因为它可能会弄乱消息的顺序。在这种情况下,我将消息排队在FIFO列表中。我将使用ThreadPool将它们出列并通过SendAsync发送。

2 个答案:

答案 0 :(得分:1)

这里有几个红旗。如果你在保持线程活动时遇到麻烦,那么你就有了一个没有做任何有用的线程。在这种情况下,使用异步方法没有意义。如果速度是您唯一关注的问题,那么您不应该使用Async方法。它们要求Socket抓取一个tp线程来进行回调。这是一个很小的开销,但如果常规线程进行阻塞调用,则 none

当您需要在处理多个同时连接时使应用程序扩展良好时,您应该只考虑异步方法。这会产生非常突发的CPU负载,非常适合tp线程。

答案 1 :(得分:0)

  

调用这些方法的线程必须保持活动状态,直到操作完成“

我不相信这句话的真实性。当您启动网络操作时,会将其传递给Windows IO系统。当网络事件发生时(即收到数据并通知您的应用程序),这可以由另一个线程处理(即使启动线程早已死亡)。

你确定你没有删除对重要内容的引用并允许它被垃圾收集吗?

如果您处于允许线程死亡的情况,那么我怀疑您的代码还没有准备好扩展到超级快速/恒星#客户端。如果您真的想要扩展,那么您应该非常严格地控制线程的使用。让线程死亡的成本(我假设另一个需要旋转以替换它)不是你应该允许发生太多的成本。线程池绝对是去这里的方式。