线程安全锁定自由相互字节数组队列

时间:2010-04-10 11:26:43

标签: .net multithreading thread-safety queue lock-free

应传输字节流,并且有一个生产者线程和一个消费者线程。 生产者的速度大多数时候都高于消费者,我需要足够的缓冲数据来保证我的应用程序的QoS。 我读到了我的问题,并且有一些解决方案,比如共享缓冲区,PipeStream .NET类... 这个类将在服务器上多次实例化,因此我需要并优化解决方案。 使用ByteArray队列是个好主意吗?

如果是,我将使用优化算法来猜测队列大小和每个ByteArray容量,理论上它适合我的情况。

如果不是,我最好的方法是什么?

如果在C#或VB中有一个很好的无锁线程安全实现ByteArray Queue,请告诉我。

提前致谢

6 个答案:

答案 0 :(得分:3)

如果不是逐字节地生成和消耗,你可能会获得更多的加速,你可以使用块。在这种情况下,代码的“锁定”可能根本不重要 - 事实上,传统的锁定解决方案可能更可取。我会尝试演示。

C#中提供了无锁,生成器,使用者,有界队列。 (清单A)
没有深奥的互锁操作,甚至没有明确的记忆障碍。让我们说,乍一看它的速度和锁定速度一样快。不是吗? 现在让我们将其与锁定解决方案that Marc Gravell has given, here进行比较。

我们将使用在核心之间没有共享L3缓存的双CPU机器。 我们预计最多加速2倍。 2倍的加速确实意味着无锁解决方案在理论界限上表现理想 为了为无锁代码创建理想的环境,我们甚至可以使用here中的实用程序类来设置生产者和使用者线程的CPU亲和性。 得到的测试代码在(清单B)中。

正在生产大约一个线程上10MBytes,而另一个线程上使用它 队列大小固定为32KBytes。如果它已满,则制作人等待 我机器上的典型测试运行如下:

  

LockFreeByteQueue:799ms
  ByteQueue:1843ms

无锁队列更快。哇,它快2倍多!这是吹嘘的事情。 :)
让我们来看看发生了什么。 Marc的锁定队列就是这么做的。它锁定。它为每个字节执行此操作。

我们真的需要锁定每个字节并逐字节推送数据吗?它肯定会在网络上以块的形式到达(就像一些大约1k的数据包)。即使它真的从内部源逐字节到达,生产者也可以轻松地将其打包成漂亮的块 让我们这样做 - 而不是逐字节地生成和使用,让我们在块中工作并将另外两个测试添加到微基准测试(清单C,只需将其插入基准测试体)。 现在典型的运行如下:

  

LockFreePageQueue:33ms
  PageQueue:25ms

现在,它们实际上比原始无锁代码快20倍 - Marc's solution添加的分块实际上更快现在比使用分块的无锁代码!登记/> 我们尝试了另一种解决方案,它可以在锁定速度下运行良好,并且可以实现20倍(!)的加速,而不是采用无锁结构,从而实现2倍的加速。 许多问题的关键不在于避免锁定 - 它更多的是避免共享和最小化锁定。在上面的例子中,我们可以避免在字节复制期间共享 我们可以在大多数时间处理私有结构,然后将单个指针排入队列,从而缩短共享空间和时间,将单个指针插入队列。

清单A,一个无锁,单生成器,单个使用者队列:

public class BoundedSingleProducerSingleConsumerQueue<T>
{
    T[] queue;
    volatile int tail;
    volatile int head;

    public BoundedSingleProducerSingleConsumerQueue(int capacity)
    {
        queue = new T[capacity + 1];
        tail = head = 0;
    }

    public bool TryEnqueue(T item)
    {
        int newtail = (tail + 1) % queue.Length;
        if (newtail == head) return false;
        queue[tail] = item;
        tail = newtail;
        return true;
    }

    public bool TryDequeue(out T item)
    {
        item = default(T);
        if (head == tail) return false;
        item = queue[head];
        queue[head] = default(T);
        head = (head + 1) % queue.Length;
        return true;
    }
}

清单B,微观基准:

class Program
{
    static void Main(string[] args)
    {
        for (int numtrials = 3; numtrials > 0; --numtrials)
        {
            using (ProcessorAffinity.BeginAffinity(0))
            {
                int pagesize = 1024 * 10;
                int numpages = 1024;
                int totalbytes = pagesize * numpages;

                BoundedSingleProducerSingleConsumerQueue<byte> lockFreeByteQueue = new BoundedSingleProducerSingleConsumerQueue<byte>(1024 * 32);
                Stopwatch sw = new Stopwatch();
                sw.Start();
                ThreadPool.QueueUserWorkItem(delegate(object state)
                {
                    using (ProcessorAffinity.BeginAffinity(1))
                    {
                        for (int i = 0; i < totalbytes; i++)
                        {
                            while (!lockFreeByteQueue.TryEnqueue((byte)(i & 0xFF))) ;
                        }
                    }
                });
                for (int i = 0; i < totalbytes; i++)
                {
                    byte tmp;
                    while (!lockFreeByteQueue.TryDequeue(out tmp)) ;
                }
                sw.Stop();
                Console.WriteLine("LockFreeByteQueue: {0}ms", sw.ElapsedMilliseconds);


                SizeQueue<byte> byteQueue = new SizeQueue<byte>(1024 * 32);
                sw.Reset();
                sw.Start();
                ThreadPool.QueueUserWorkItem(delegate(object state)
                {
                    using (ProcessorAffinity.BeginAffinity(1))
                    {
                        for (int i = 0; i < totalbytes; i++)
                        {
                            byteQueue.Enqueue((byte)(i & 0xFF));
                        }
                    }
                });

                for (int i = 0; i < totalbytes; i++)
                {
                    byte tmp = byteQueue.Dequeue();
                }
                sw.Stop();
                Console.WriteLine("ByteQueue: {0}ms", sw.ElapsedMilliseconds);

                Console.ReadKey();
            }
        }
    }
}

列表C,分块测试:

BoundedSingleProducerSingleConsumerQueue<byte[]> lockfreePageQueue = new BoundedSingleProducerSingleConsumerQueue<byte[]>(32);
sw.Reset();
sw.Start();
ThreadPool.QueueUserWorkItem(delegate(object state)
{
    using (ProcessorAffinity.BeginAffinity(1))
    {
        for (int i = 0; i < numpages; i++)
        {
            byte[] page = new byte[pagesize];
            for (int j = 0; j < pagesize; j++)
            {
                page[j] = (byte)(i & 0xFF);
            }
            while (!lockfreePageQueue.TryEnqueue(page)) ;
        }
    }
});
for (int i = 0; i < numpages; i++)
{
    byte[] page;
    while (!lockfreePageQueue.TryDequeue(out page)) ;
    for (int j = 0; j < pagesize; j++)
    {
        byte tmp = page[j];
    }
}
sw.Stop();
Console.WriteLine("LockFreePageQueue: {0}ms", sw.ElapsedMilliseconds);

SizeQueue<byte[]> pageQueue = new SizeQueue<byte[]>(32);

ThreadPool.QueueUserWorkItem(delegate(object state)
{
    using (ProcessorAffinity.BeginAffinity(1))
    {
        for (int i = 0; i < numpages; i++)
        {
            byte[] page = new byte[pagesize];
            for (int j = 0; j < pagesize; j++)
            {
                page[j] = (byte)(i & 0xFF);
            }
            pageQueue.Enqueue(page);
        }
    }
});
sw.Reset();
sw.Start();
for (int i = 0; i < numpages; i++)
{
    byte[] page = pageQueue.Dequeue();
    for (int j = 0; j < pagesize; j++)
    {
        byte tmp = page[j];
    }
}
sw.Stop();
Console.WriteLine("PageQueue: {0}ms", sw.ElapsedMilliseconds);

答案 1 :(得分:2)

在.NET 4中,有System.Collections.Concurrent.Queue<T>这些东西可以锁定(虽然仍然是通用的)。

答案 2 :(得分:2)

博士。 Dobbs实现了一个无锁队列in C++,您可以相对轻松地采用C#。它只适用于一个生产者(可以有任意数量的消费者)。

基本思想是使用双向链表作为底层结构以及可移动的头部和尾部参考。生成一个项目时,它会被添加到结尾,并且列表开头和当前“头部”之间的所有内容都将被删除。消费,试图抬起头;如果它击中尾部,则失败,如果没有,则成功并返回新元素。特定的操作顺序使其本身具有线程安全性。

然而,在这里使用这种“无锁”设计存在两个主要问题:

  1. 无法强制执行队列大小的上限,如果您的生产者比您的消费者更快,这可能是一个严重的问题;

  2. 按照设计,如果没有生成任何内容,Consume方法必须完全无法检索元素。这意味着您需要为消费者实现自己的锁定,并且这种锁定总是要么忙碌(等于比在性能范围内锁定更差),要么定时等待(这进一步减慢了你的消费者的速度)。

  3. 由于这些原因,我建议您认真考虑是否真的需要无锁结构。许多人来到这个网站认为它会比使用锁定的等效结构“更快”,但大多数应用程序的实际差异是微不足道的,通常不值得增加复杂性,在某些情况下它实际上可以执行更糟,因为等待状态(或警报等待)比忙等待便宜得多。

    多核机器和对内存屏障的需求使得有效的无锁线程变得更加复杂;在正常操作下,您仍然可以执行乱序执行,而在.NET中,抖动可以进一步决定重新排序指令,因此您可能需要使用volatile变量和Thread.MemoryBarrier来编写代码。调用,这可能有助于使无锁版本比基本同步版本更昂贵。

    如何首先使用普通的同步生产者 - 消费者队列,并分析您的应用程序,以确定它是否能满足您的性能要求?在Joseph Albahari's site处有一个非常有效的P-C队列实现。或者,正如Richard所提到的,如果您使用的是.NET 4.0框架,那么您只需使用ConcurrentQueue或更可能使用BlockingCollection

    首先测试 - 加载测试同步队列,这很容易实现 - 并观察实际花费了多长时间锁定。不等待,无论如何都要做,但实际上获取在发出信号后发布锁。如果它超过程序执行时间的1%,我会非常惊讶;但如果是这样,那么然后开始关注无锁实现 - 并确保你也对它们进行分析,以确保它们实际上表现得更好。

答案 3 :(得分:1)

限制在这里很重要,通过它的声音,这个magazine article中的BoundedBuffer类符合要求。类似的类将在.NET 4.0中作为BlockingCollection class提供。调整缓冲区大小仍然取决于你。

答案 4 :(得分:0)

Julian M Bucknall written one in C#

答案 5 :(得分:0)

最重要的部分是共享对象的设计。在我的场景中,读者和编写者可以独立使用单独的缓冲区(大数据块),然后只应访问像队列一样的共享FIFO对象。这样,锁定时间最小化,线程可以并行完成作业。并且通过 .NET framewok 4.0 实现这一概念变得简单:

System.Collections.Concurrent命名空间中有一个ConcurrentQueue(Of T)类,而arrayByte是一个很好的类型,可用作我的场景的队列类型。命名空间中还有其他线程安全的集合。

http://msdn.microsoft.com/en-us/library/system.collections.concurrent.aspx