我正在实施一个小型库,以便更轻松地使用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
以下是我打算使用的解决方案:
我让库用户的线程直接调用XXXAsync
(SendAsync
除外)操作。在大多数情况下,调用将是成功的(因为调用线程很少终止)
如果操作失败并显示SocketError.OperationAborted
,则库仅使用异步回调中的当前线程再次调用该操作。这个是一个ThreadPool
线程,它有很好的成功机会(如果SocketError.OperationAborted
的原因是由于某些其他错误导致的原因,将设置一个额外的标志,最多使用此解决方法一次) 。
这应该工作,因为套接字本身仍然正常,只是前一个操作失败。
对于SendAsync
,此解决方法不起作用,因为它可能会弄乱消息的顺序。在这种情况下,我将消息排队在FIFO列表中。我将使用ThreadPool
将它们出列并通过SendAsync
发送。
答案 0 :(得分:1)
这里有几个红旗。如果你在保持线程活动时遇到麻烦,那么你就有了一个没有做任何有用的线程。在这种情况下,使用异步方法没有意义。如果速度是您唯一关注的问题,那么您不应该使用Async方法。它们要求Socket抓取一个tp线程来进行回调。这是一个很小的开销,但如果常规线程进行阻塞调用,则 none 。
当您需要在处理多个同时连接时使应用程序扩展良好时,您应该只考虑异步方法。这会产生非常突发的CPU负载,非常适合tp线程。
答案 1 :(得分:0)
调用这些方法的线程必须保持活动状态,直到操作完成“
我不相信这句话的真实性。当您启动网络操作时,会将其传递给Windows IO系统。当网络事件发生时(即收到数据并通知您的应用程序),这可以由另一个线程处理(即使启动线程早已死亡)。
你确定你没有删除对重要内容的引用并允许它被垃圾收集吗?
如果您处于允许线程死亡的情况,那么我怀疑您的代码还没有准备好扩展到超级快速/恒星#客户端。如果您真的想要扩展,那么您应该非常严格地控制线程的使用。让线程死亡的成本(我假设另一个需要旋转以替换它)不是你应该允许发生太多的成本。线程池绝对是去这里的方式。