我有一台服务器,可以通过TCP LAN与50个或更多设备进行通信。每个套接字读取消息循环都有一个Task.Run。
我将每个消息缓冲到一个阻塞队列中,每个阻塞队列中都有一个Task.Run,使用BlockingCollection.Take()。
类似(半伪代码)的东西
套接字读取任务
Task.Run(() =>
{
while (notCancelled)
{
element = ReadXml();
switch (element)
{
case messageheader:
MessageBlockingQueue.Add(deserialze<messageType>());
...
}
}
});
消息缓冲任务
Task.Run(() =>
{
while (notCancelled)
{
Process(MessageQueue.Take());
}
});
因此,这将使50多个读取任务和50多个任务在其自己的缓冲区上阻塞。
我这样做是为了避免阻塞阅读循环,并允许程序更公平地分配对消息的处理时间。
这是处理效率低下的方法吗?有什么更好的方法?
答案 0 :(得分:4)
您可能对“渠道”工作感兴趣,尤其是:System.Threading.Channels。这样做的目的是提供提供者异步生产者/消费者队列,涵盖单个和多个生产者和消费者方案,上限等。通过使用异步API,您不会占用很多线程只是在等待做某事。
您的读取循环将变为:
while (notCancelled) {
var next = await queue.Reader.ReadAsync(optionalCancellationToken);
Process(next);
}
和制作人:
switch (element)
{
case messageheader:
queue.Writer.TryWrite(deserialze<messageType>());
...
}
所以:变化很小
或者,或者结合起来,您可以研究“管道”(https://www.nuget.org/packages/System.IO.Pipelines/)之类的东西-因为您正在处理TCP数据,所以这是一个理想的选择,我已经研究过在此处的Stack Overflow(用于处理巨大数量的连接)上的自定义Web套接字服务器。由于API始终是异步的,因此在平衡工作方面做得很好-管道API在设计时考虑了典型的TCP场景,例如,在检测帧边界时部分消耗了传入的数据流。我已经写了很多有关这种用法的文章,其中大部分是代码示例here。请注意,“管道”不包括直接的TCP层,但是“红est”服务器包括一个,或者第三方库https://www.nuget.org/packages/Pipelines.Sockets.Unofficial/包括(公开:我写了它)。
答案 1 :(得分:1)
我实际上在另一个项目中做了类似的事情。我学到的或将做的不同的事情如下:
首先,最好将专用线程用于读/写循环(与new Thread(ParameterizedThreadStart)
一起使用,因为Task.Run
使用池线程,并且您在(几乎)无尽的线程中使用它)循环线程几乎永远不会返回到池中。
var thread = new Thread(ReaderLoop) { Name = nameof(ReaderLoop) }; // priority, etc if needed
thread.Start(cancellationToken);
您的Process
可以是一个事件,您可以异步调用该事件,以便您的阅读器循环可以立即返回,以尽可能快地处理新的传入软件包:
private void ReaderLoop(object state)
{
var token = (CancellationToken)state;
while (!token.IsCancellationRequested)
{
try
{
var message = MessageQueue.Take(token);
OnMessageReceived(new MessageReceivedEventArgs(message));
}
catch (OperationCanceledException)
{
if (!disposed && IsRunning)
Stop();
break;
}
}
}
请注意,如果一个委托有多个目标,那么异步调用就不容易了。我创建了这种扩展方法来在池线程上调用委托:
public static void InvokeAsync<TEventArgs>(this EventHandler<TEventArgs> eventHandler, object sender, TEventArgs args)
{
void Callback(IAsyncResult ar)
{
var method = (EventHandler<TEventArgs>)ar.AsyncState;
try
{
method.EndInvoke(ar);
}
catch (Exception e)
{
HandleError(e, method);
}
}
foreach (EventHandler<TEventArgs> handler in eventHandler.GetInvocationList())
handler.BeginInvoke(sender, args, Callback, handler);
}
因此OnMessageReceived
实现可以是:
protected virtual void OnMessageReceived(MessageReceivedEventArgs e)
=> messageReceivedHandler.InvokeAsync(this, e);
最后,BlockingCollection<T>
遇到了一些性能问题,这是一个重要的教训。它在内部使用SpinWait
,如果长时间没有输入数据,其SpinOnce
方法将等待越来越长的时间。这是一个棘手的问题,因为即使您记录了处理的每个步骤,除非您也可以模拟服务器端,否则您不会注意到所有启动都被延迟了。 Here可以找到一个BlockingCollection
来触发输入数据的快速AutoResetEvent
实现。我向其添加了Take(CancellationToken)
重载,如下所示:
/// <summary>
/// Takes an item from the <see cref="FastBlockingCollection{T}"/>
/// </summary>
public T Take(CancellationToken token)
{
T item;
while (!queue.TryDequeue(out item))
{
waitHandle.WaitOne(cancellationCheckTimeout); // can be 10-100 ms
token.ThrowIfCancellationRequested();
}
return item;
}
基本上就是这样。也许并非所有情况都适用于您的情况,例如如果几乎即时的响应不是很关键,那么常规的BlockingCollection
也会这样做。
答案 2 :(得分:1)
是的,这有点效率低下,因为您阻塞了ThreadPool线程。 我已经讨论了这个问题Using Task.Yield to overcome ThreadPool starvation while implementing producer/consumer pattern
您还可以在此处查看测试生产者-消费者模式的示例: https://github.com/BBGONE/TestThreadAffinity
您可以在循环中使用await Task.Yield来授予其他任务对该线程的访问权限。
您也可以通过使用专用线程来解决它,或者更好地使用它自己的线程池的自定义ThreadScheduler。但是创建50个以上的普通线程是无效的。更好地调整任务,这样会更加合作。
如果您使用BlockingCollection(因为它可能在等待写入(如果受限制)或读取或不读取任何项目的同时阻塞线程很长时间),则最好使用 System.Threading.Tasks.Channels https://github.com/stephentoub/corefxlab/blob/master/src/System.Threading.Tasks.Channels/README.md
在等待何时可以使用集合进行写入或读取时,它们不会阻塞线程。有一个示例https://github.com/BBGONE/TestThreadAffinity/tree/master/ThreadingChannelsCoreFX/ChannelsTest
的用法