0

I am trying to create a form of Buffered Input to see how easy it would be to implement, without the use of Rx or any other library (outside standard .net 4.5). So I came up with the following class:

public class BufferedInput<T>
{
    private Timer _timer;
    private volatile Queue<T> _items = new Queue<T>();

    public event EventHandler<BufferedEventArgs<T>> OnNext;

    public BufferedInput() : this(TimeSpan.FromSeconds(1))
    {
    }
    public BufferedInput(TimeSpan interval)
    {
        _timer = new Timer(OnTimerTick);
        _timer.Change(interval, interval);
    }

    public void Add(T item)
    {
        _items.Enqueue(item);
    }

    private void OnTimerTick(object state)
    {
#pragma warning disable 420
        var bufferedItems = Interlocked.Exchange(ref _items, new Queue<T>());
        var ev = OnNext;
        if (ev != null)
        {
            ev(this, new BufferedEventArgs<T>(bufferedItems));
        }
#pragma warning restore 420
    }
}

The principal being, that once the timer ticks it switches the queues and carries on triggering the event. I realise that this could have been done with a list...

After a while i get the following, familiar, exception:

Collection was modified after the enumerator was instantiated.

On the following line:

public BufferedEventArgs(IEnumerable<T> items) : this(items.ToList())

The declaration and test program are:

public sealed class BufferedEventArgs<T> : EventArgs
{
    private readonly ReadOnlyCollection<T> _items;
    public ReadOnlyCollection<T> Items { get { return _items; } }

    public BufferedEventArgs(IList<T> items)
    {
        _items = new ReadOnlyCollection<T>(items);
    }

    public BufferedEventArgs(IEnumerable<T> items) : this(items.ToList()) 
    {
    }
}

class Program
{
    static void Main(string[] args)
    {
        var stop = false;
        var bi = new BufferedInput<TestClass>();

        bi.OnNext += (sender, eventArgs) =>
        {
            Console.WriteLine(eventArgs.Items.Count + " " + DateTime.Now);
        };

        Task.Run(() =>
        {
            var id = 0;
            unchecked
            {
                while (!stop)
                {
                    bi.Add(new TestClass { Id = ++id });
                }
            }
        });

        Console.ReadKey();
        stop = true;
    }
}

My thought was that after the call to Interlocked.Exchange (an atomic operation) took place, a call to _items would return the new collection. But there seems to a gremlin in the works...

Stuart Blackler
  • 3,732
  • 5
  • 35
  • 60

1 Answers1

1

after the call to Interlocked.Exchange (an atomic operation) took place, a call to _items would return the new collection

Well, that's true. But the read of _items took place before the call to Interlocked.Exchange.

This line of code

_items.Enqueue(item);

turns into multiple MSIL instructions, roughly:

ldthis ; really ldarg.0
ldfld _items
ldloc item
callvirt Queue<T>::Enqueue

If the InterlockedExchange happens between the second and fourth instruction, or any time during execution of the Enqueue method, BAM!

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • Hmm, I'm guessing what I am trying to do just isn't possible without a locking primitive? – Stuart Blackler Oct 16 '14 at 22:00
  • @Stuart: Probably not. You have this entire interval where the producer is fiddling with the Queue, that it can't be interrupted. **It is possible to make a lockfree single consumer queue** (I've done it), but you won't be able to use the existing `Queue` class for that. – Ben Voigt Oct 16 '14 at 22:02
  • is it possible to see that implementation? I did actually have a go at this my self earlier, but I was 50% ish slower than a Queue with locking. So I don't think i did something quiet right (using a linked list approach, didn't try an array based method); – Stuart Blackler Oct 16 '14 at 22:26
  • It was native C++, but basically it is a singly linked list and interlocked compare-exchange to replace the head on the producer side, interlocked exchange to take ownership of the whole list on the consumer side. Of course the items are in reverse order at that point, so the consumer reverses the order again before passing the list to the app. – Ben Voigt Oct 17 '14 at 00:42