Socket.ReceiveAsync – broken or broken?

I ran into another .Net runtime bug today.  Socket.ReceiveAsync.  This method promises to improve performance compared to using Socket.BeginReceive/EndReceive and, I would say it manages that.  However, if you are receiving data into continuously moving buffer, ReceiveAsync is dangerous.  Specifically if you call BufferList setter or SetBuffer after each receive before starting the next, it returns seemingly random values for the number of bytes transferred compared to what is written in the actual buffer.

After many hours of reflection and debugging I think I may finally understand what is going wrong.  It appears that ReceiveAsync is ‘too fast’, an asynchronous completion can be triggered before ReceiveAsync exits, presumably while WSARecv is still running.  Then when BufferList gets set it modifies stuff that WSARecv is still using, causing much confusion and something goes horribly wrong…

I think BeginReceive/EndReceive don’t suffer from this problem because they construct a new set of parameters for WSARecv every time rather than attempting to reuse stuff to avoid memory allocations, so there is no potential for a still active WSARecv to mess around with the data about to be given to the next WSARecv.  Either that or EndReceive ends up waiting in what would be the failure case for ReceiveAsync – but the reflected code doesn’t seem to indicate that.

The ‘solution’ I have for ReceiveAsync relies on waiting to reset BufferList until a ManualResetEventSlim gets set when the previous call to ReceiveAsync returns.  This solves the problems, but even ManualResetEventSlim is expensive enough that it seems the performance gains are lost.

Hacky Ugly Example Code:  (Note that removing the comment marks on the lines regarding the ManualResetEventSlim ensures this program never prints anything.)

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace ConsoleApplication8
{
    class Program
    {
        static void Main(string[] args)
        {
            TcpListener listener = new TcpListener(IPAddress.Any, 5678);
            listener.Start();
            client = new TcpClient("localhost", 5678);
            var server = listener.AcceptTcpClient();
            ThreadPool.QueueUserWorkItem(callback =>
                {
                    byte[] bufferInner = new byte[8192];
                    while (true)
                    {
                        try
                        {
                            int bytes = server.Client.Receive(bufferInner);
                            if (bytes == 0)
                                return;
                            server.Client.Send(bufferInner, 0, bytes, SocketFlags.None);
                        }
                        catch
                        {
                            break;
                        }
                    }

                });
            ThreadPool.QueueUserWorkItem(callback =>
                {
                    byte[] bufferInner = new byte[24];
                    for (int i = 0; i < bufferInner.Length; i++)
                        bufferInner[i] = (byte) (i+1);
                    for (int i = 0; i < 10000000; i++)
                        client.Client.Send(bufferInner);
                });
            asyncEventArgs1 = new SocketAsyncEventArgs();
            asyncEventArgs1.Completed += AsyncEventArgsOnCompleted;
            asyncEventArgs2 = new SocketAsyncEventArgs();
            asyncEventArgs2.Completed += AsyncEventArgsOnCompleted;
            buffer = new byte[bigBufferLength];
            BeginReceive();
            while (true)
            {
                Console.ReadKey();
            }
        }

        private const int bigBufferLength = 1024*1024;

        private static void BeginReceive()
        {
            if (asyncEventArgs == asyncEventArgs1)
                asyncEventArgs = asyncEventArgs2;
            else
                asyncEventArgs = asyncEventArgs2;
            int length = rnd.Next(2) == 0 ? 8192 : 100;
            prevLastWrapped = lastWrapped;
            lastWrapped = false;
            if (lastWriteIndex + length <= bigBufferLength)
                asyncEventArgs.BufferList = new ArraySegment<byte>[]
                    {new ArraySegment<byte>(buffer, lastWriteIndex, length),};
            else
            {
                asyncEventArgs.BufferList = new ArraySegment<byte>[]
                    {
                        new ArraySegment<byte>(buffer, lastWriteIndex, bigBufferLength - lastWriteIndex),
                        new ArraySegment<byte>(buffer, 0, lastWriteIndex + length - bigBufferLength),
                    };
                lastWrapped = true;
            }
            Clear(length);
            prevLastWriteLength = lastWriteLength;
            lastWriteLength = length;
            prevWasSync = wasSync;
            wasSync = false;
            int newVal = Interlocked.Decrement(ref counter);
            if (newVal != -1)
            {
                Console.WriteLine("BeginReceive called wrong number of times. {0}", newVal);
                throw new Exception("Fail!");
            }
            //callerExited.Reset();
            if (!client.Client.ReceiveAsync(asyncEventArgs))
            {
                wasSync = true;
                //callerExited.Set();
                AsyncEventArgsOnCompleted(null, asyncEventArgs);
            }
            else
            {
                //callerExited.Set();
            }
        }
        //private static ManualResetEventSlim callerExited = new ManualResetEventSlim(false);

        private static bool lastWrapped = false;
        private static bool prevLastWrapped = false;

        private static bool wasSync = false;
        private static bool prevWasSync = false;

        private static volatile int counter = 0;

        private static void Clear(int length)
        {
            if (lastWriteIndex + length <= bigBufferLength)
                Array.Clear(buffer, lastWriteIndex, length);
            else
            {
                Array.Clear(buffer, lastWriteIndex, bigBufferLength - lastWriteIndex);
                Array.Clear(buffer, 0, lastWriteIndex + length - bigBufferLength);                
            }
        }

        private static void AsyncEventArgsOnCompleted(object sender, SocketAsyncEventArgs socketAsyncEventArgs)
        {
            int newVal = Interlocked.Increment(ref counter);
            if (newVal != 0)
            {
                Console.WriteLine("OnCompleted called wrong number of times. {0}", newVal);
                throw new Exception("Fail!");
            }

            if (socketAsyncEventArgs.SocketError != SocketError.Success)
                return;
            if (socketAsyncEventArgs.BytesTransferred == 0)
                return;
            if (socketAsyncEventArgs.BytesTransferred > lastWriteLength)
            {
                Console.WriteLine("Receive too long, Sync:{0}:{1} LastWrapped:{2}:{3} Length:{4}:{5} ClaimedLength:{6}", wasSync, prevWasSync, lastWrapped, prevLastWrapped, lastWriteLength, prevLastWriteLength, socketAsyncEventArgs.BytesTransferred);
            }
            if (buffer[(socketAsyncEventArgs.BytesTransferred-1 + lastWriteIndex) % bigBufferLength] == 0)
            {
                Console.WriteLine("Receive too short-quickcheck , Sync:{0}:{1} LastWrapped:{2}:{3} Length:{4}:{5}", wasSync, prevWasSync, lastWrapped, prevLastWrapped, lastWriteLength, prevLastWriteLength);
            }
            //for (int i = 0; i < socketAsyncEventArgs.BytesTransferred; i++)
            //{
            //    if (buffer[(i + lastWriteIndex) % bigBufferLength] == 0)
            //    {
            //        Console.WriteLine("Receive too short , Sync:{0}:{1} LastWrapped:{2}:{3} Length:{4}:{5} ActualLength:{6}", wasSync, prevWasSync, lastWrapped, prevLastWrapped, lastWriteLength, prevLastWriteLength, i);
            //        break;
            //    }
            //}
            lastWriteIndex = (lastWriteIndex + socketAsyncEventArgs.BytesTransferred)%bigBufferLength;
            //callerExited.Wait();
            BeginReceive();
        }

        private static SocketAsyncEventArgs asyncEventArgs;
        private static SocketAsyncEventArgs asyncEventArgs1;
        private static SocketAsyncEventArgs asyncEventArgs2;
        private static byte[] buffer;
        private static int lastWriteIndex;
        private static int lastWriteLength;
        private static int prevLastWriteLength = 0;
        private static Random rnd =new Random();
        private static TcpClient client;
    }
}

8 thoughts on “Socket.ReceiveAsync – broken or broken?”

  1. I never had prolems using the ReceiveAsync and I think your problem is that you are doing more than one ReceiveAsync over the same buffer.

    A normal error is to put a new ReceiveAsync at the beginning of the callback. You should never do that. You should first process your data and then, at the end, call another ReceiveAsync. As the data was already read, there is no risk of the new ReceiveAsync starting to change your buffer when you are still using it.

  2. To ensure clarity, I have added my actual test case which shows that things are broken.

    If you do sufficient processing, the problem will certainly go away, but in the use case of receiving into a circular buffer (like in the example), the amount of processing performed is tiny and despite calling ReceiveAsync as the very last thing I do in each callback, I still get corruption.
    I even tried using two async event args and switch back and forth between them, even that doesn’t slow things down enough to guarantee the problem never occurs.

  3. Good find. I’ve looked at the test case you’ve shown and have come to agree with you. There’s a bug in the test code, although I think this illustrates your point quite well:


    if (asyncEventArgs == asyncEventArgs1)
    asyncEventArgs = asyncEventArgs2;
    else
    asyncEventArgs = asyncEventArgs2;

    The second assignment should be assigning asyncEventArgs1; currently only asyncEventArgs2 is ever used.

    Without the fix, this test raises frequent errors, roughly 2 – 3 per second. With the fix, I can run the test for minutes at a time without errors. However, errors still occur, just much less frequently.

    The fact that the error frequency changes dramatically when using an additional args object indicates an underlying timing issue with the way SocketAsyncEventArgs is managing the buffer, at least when using BufferList.

    I’m not sure how else this can be safely solved. At least this is (probably) still more efficient than Begin/EndReceive.

    Thanks for the post. This will help me avoid some problems.

  4. Good catch on the args2 and args2 – I forgot I had changed that to be a no-op during my pre-post double check – but yes as you say, it still fails when you actually switch between them, just less frequently.

  5. After testing your test code and I was quite shocked that there was a severe problem you described. And I found out that the problem disappears with using simple SetBufffer instead of using BufferList for a circular buffer. Because SetBuffer doesn’t invalidate an overlapped object used by WSARecv.

  6. Hello,

    I’m in the hell of ReceiveAsync with fast continnuous stream of packets… I have applied your solution of the AutoResetEvent and it enhance the situation. But I still have other problem :
    – Lenght is right
    – But content is corrupted
    If I insert a delay (something like Thread.Sleep(1) but of 30 microseconds) before calling ReceiveAsync, all is OK. But if I do it immediatly content is randomly corrupted. This is incredible. I dont’t find any ressources about this problem. It’s juste incredible… Are-we alone on the earth to do ReceiveAsync with a really fast re-call of this method ? Do you notify corrupted content ?

  7. I still seem to have this problem. I’m on .NET 4.5. Getting the same error when using BeginReceive/EndReceive!
    Anyone found a good solution yet?

Leave a Reply

Your email address will not be published. Required fields are marked *