使用BlockingCollection <t>()

时间:2018-11-20 08:43:40

标签: c# multithreading performance networking parallel-processing

我有一台服务器,可以通过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多个任务在其自己的缓冲区上阻塞。

我这样做是为了避免阻塞阅读循环,并允许程序更公平地分配对消息的处理时间。

这是处理效率低下的方法吗?有什么更好的方法?

3 个答案:

答案 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)

我实际上在另一个项目中做了类似的事情。我学到的或将做的不同的事情如下:

  1. 首先,最好将专用线程用于读/写循环(与new Thread(ParameterizedThreadStart)一起使用,因为Task.Run使用池线程,并且您在(几乎)无尽的线程中使用它)循环线程几乎永远不会返回到池中。

    var thread = new Thread(ReaderLoop) { Name = nameof(ReaderLoop) }; // priority, etc if needed
    thread.Start(cancellationToken);
    
  2. 您的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);
  1. 最后,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

的用法