Fallback protection without blocking ByteArray queue

A stream of bytes needs to be passed, and there is one producer and consumer stream. Vendor speed is faster than consumer in most cases and I need enough buffered data for the QoS of my application. I read about my problem and there are solutions like shared buffer, PipeStream.NET class ... This class will be generated many times on the server, so I need an optimized solution as well. Is it good to use a ByteArray queue?

If so, I will use an optimization algorithm to guess the size of the queue and each capacity of the ByteArray, and in theory this fits my case.

If not, what's the best approach?

Please let me know if there is a good safe stream-safe implementation of ByteArray Queue in C # or VB.

Thanks in advance

+2


source to share


6 answers


The most important part is the design of the overall object. In my scenario, the reader and writer can independently use separate buffers (large chunks of data) and then only synchronize access to a shared FIFO such as a queue. Thus, blocking time is minimized and threads can execute the job in parallel. And with .NET framewok 4.0, the implementation of this concept has been simplified:

There is a ConcurrentQueue (Of T) class in the System.Collections.Concurrent space and arrayByte is a good type to use as a queue type for my scenario. There are other thread safe collections in the namespace.



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

0


source


You will probably get a lot more speedup if, instead of creating and consuming bytes, you work in chunks. In this case, the "lock-engagement" code probably doesn't matter at all — in fact, the traditional blocking solution may be preferred. I will try to demonstrate.

In C #, an unblocked, single, single, bounded queue is defined. (Listing A)
There are no esoteric blocking operations, not even explicit memory barriers. Let's say that at first glance it is as fast and without blocking as it gets. Is not it? Now compare it to the blocking solution given by Mark Gravell here .

We will be using a dual-processor machine that does not have a shared L3 cache between the cores. We expect a maximum of 2x speedup. A 2x speedup would really mean that a non-blocking solution works perfectly within the theoretical framework.
To create an ideal environment for non-blocking code, we'll even set the processor's proximity to the manufacturer and consumer thread using the utility class here.
The resulting test code is in (Listing B).

It produces approx. 10 MB in one stream, consuming it in another.
The queue size is fixed at 32 KB. If it's full, the producer waits.
A typical test run on my machine looks like this:

LockFreeByteQueue: 799ms
ByteQueue: 1843 ms

Stopping without blocking is faster. Wow, that's more than twice as fast! It's something to brag about. :)
Let's see what happens. The blocking queue mark does just that. He locks. It does this for every byte.

Do we need to block every byte and push data bytes by byte? It certainly comes in chunks on the net (like about 1,000 packages). Even if it does indeed come in bytes from an internal source, the manufacturer can easily package it into good chunks.
Let's just do this - instead of creating and consuming byte bytes, let's work in chunks and add two more micro-benchmark tests (Listing C, just paste it in the breakpoint tag).
Now a typical run looks like this:

LockFreePageQueue: 33ms
PageQueue: 25ms



Now both of them are actually 20x faster than the non-blocking source code - the Marc solution with added chunking is actually faster than the non-blocking code with chunking!
Instead of going with a locked structure that would lead to a 2x speedup, we tried to create another solution that works great with blocking and resulted in a 20x (!) Speedup.
The key to a lot of problems is not so much blocking avoidance - it's much more about avoiding sharing and minimizing blocking. In the above case, we can avoid sharing during the byte copy period.
We can work in a private framework most of the time and then insert one pointer, thus reducing the total space and time to one insertion of one pointer into the queue.

In Listing A, the unblocked, single, single user queue:

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;
    }
}

      

Listing B, micro benchmark:

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();
            }
        }
    }
}

      

In Listing C, the tests are verified:

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);

      

+3


source


In .NET 4 there is System.Collections.Concurrent.Queue<T>

one that is free from blocking as these things can be (although still are common). In the meantime, there is no need to worry about it. ”

+2


source


R. Dobbs implemented an unoccupied queue in C ++ , which you could migrate to C # relatively easily. It works when there is exactly one producer (there can be any number of consumers).

The basic idea is to use a doubly linked list as the base structure along with a movable head and tail guide. When an element is created, it is added to the end, and everything between the beginning of the list and the current head is removed. To consume, try to move your head up; if it hits the tail, it fails, if it doesn't, it succeeds and returns a new element. The particular order of operations makes it inherently thread safe.

However, there are two main problems with using such a "loose" project:

  • It is not possible to enforce an upper bound on the queue size, which can be a major problem if your manufacturer is faster than your consumer;

  • By design, the method Consume

    should just not get the element if nothing was created. This means that you need to implement your own locking on the consumer, and such a lock is always either waiting (which is much worse than locking in the performance spectrum) or timeout (which slows down your consumer even more).

For these reasons, I recommend that you seriously consider whether you really need a lock-free structure. Many people come to this site thinking that it would be "faster" than the equivalent structure using locking, but the practical difference for most applications is so negligible that it is usually not worth the extra complexity, and in some cases it can actually be worse because waiting states (or anxious expectations) are much cheaper than waiting.

Multicore machines and the need for memory barriers make effective locking threads even more challenging; in normal operation, you can still crash, and in .NET, jitter can additionally decide to reorder instructions, so you probably need to port your code with volatile

variables and Thread.MemoryBarrier

calls, which again can help make the non-blocking version more expensive than the basic sync version.

How do you use a plain old synchronized producer-consumer queue and profile your application to determine if it can meet your performance requirements? There's a great, efficient implementation of the PC queue on Joseph Albahari's site . Or, as Richard mentions, if you are using the .NET 4.0 framework, you can simply use ConcurrentQueue, or most likely BlockingCollection .

Test first - load a test synchronized queue that is easy to implement - and see how much time it actually took to lock. Don't expect to have to do anyway, but actually acquire and release locks after they become signaling. If this is more than 1% of the execution time of your program, I would be very surprised; but if so, then start looking at locks and also make sure you profile them too to make sure they actually perform better.

+2


source


Throttling is important here, as it sounds, the BoundedBuffer class in this article is in line with the bill. A similar class will be available in .NET 4.0 as the BlockingCollection class . Setting the buffer size is still up to you.

+1


source


Julian M Bucknall has it written in C # .

0


source







All Articles