1

Given the simple socket client class below, connect it to a TCP server (I use SocketTest3, freely available online). Then disconnect the server and wait for a bit. You should get a LockRecursionException.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

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

using System.Threading;

namespace SocketRwlTest
{
    public class SocketRwlTest
    {
        private Socket client = new Socket(AddressFamily.InterNetwork,
                                           SocketType.Stream,
                                           ProtocolType.Tcp);
        private readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();
        private const int maxLength = 200;

        public SocketRwlTest(IPAddress address, ushort port)
        {
            client.Connect(new IPEndPoint(address, port));
            ReceiveOne();
        }

        private void ReceiveOne()
        {
            rwl.EnterReadLock();
            try
            {
                var inArray = new byte[maxLength];
                client.BeginReceive(inArray, 0, maxLength, 0,
                                    new AsyncCallback(ReceivedCallback),
                                    inArray);
            }
            finally
            {
                rwl.ExitReadLock();
            }
        }

        private void ReceivedCallback(IAsyncResult ar)
        {
            client.EndReceive(ar);
            ReceiveOne();
        }
    }
}

I don't understand why it happens in the simplified example given. I know I should stop calling ReceiveOne as soon as I receive a zero-length message, but this is more of an exercise. I wondered if a similar bug could maintain a constant stream of callbacks running in the background and stealing resources without obviously bad things happening. I must admit I wasn't expecting this particular exception.

Question 1: Why does this happen? Are BeginXYZ methods perhaps allowed to execute callbacks instantly, on the same thread? If that's the case, who's to say this couldn't happen during normal runtime?

Question 2: Are there ways to avoid getting this exception while still maintaining the "desired" behaviour in this case? I mean fire a non-stopping stream of callbacks.

I'm using Visual Studio 2010 with .NET 4.

relatively_random
  • 4,505
  • 1
  • 26
  • 48
  • I think you are missing a call to `client.EndReceive` in your `ReceiveCallback` method (though that doesn't answer the question) – René Vogt Jan 21 '16 at 18:13
  • 2
    The [reference source](http://referencesource.microsoft.com/#System/net/System/Net/Sockets/Socket.cs,12174aa527fd9499) certainly indicates that BeginReceive is designed to possibly execute on a single thread: "We kick off the receive, and if it completes synchronously we'll call the callback. Otherwise we'll return an IASyncResult, which the caller can use to wait on or retrieve the final status, as needed." I don't see anything in the docs or the source code that would make me think that BeginReceive was *required* to call the callback on another thread. – Mike Zboray Jan 21 '16 at 18:29
  • I wasn't aware of that. It makes perfect sense, actually. Performance-wise. So that basically means you cannot use non-recursive locks when the callback is calling BeginReceive. Which is sort of a recursion anyway so it makes sense. :) – relatively_random Jan 21 '16 at 19:24

1 Answers1

1

Question 1: Why does this happen? Are BeginXYZ methods perhaps allowed to execute callbacks instantly, on the same thread? If that's the case, who's to say this couldn't happen during normal runtime?

As described by mike z in the comments, the BeginReceive() method is not required to execute asynchronously. If data is available it will execute synchronously, calling the callback delegate in the same thread. This is by definition a recursive call, and so would not be compatible with the use of a non-recursive lock object (such as the ReaderWriterLockSlim you're using here).

This certainly can happen "during normal runtime". I'm not sure I understand the second part of your question. Who's to say it can't happen? No one. It can happen.

Question 2: Are there ways to avoid getting this exception while still maintaining the "desired" behaviour in this case? I mean fire a non-stopping stream of callbacks.

I'm afraid I also don't know what you mean by "fire a non-stopping stream of callbacks".

One obvious workaround is to enable recursion on the ReaderWriterLockSlim object by passing LockRecursionPolicy.SupportsRecursion to its constructor. Alternatively, you could check the IsReadLockHeld property before trying to take the lock.

It is not clear from your code example why you have the lock at all, never mind why it's used in that specific way. It's possible the right solution is to not hold the lock at all while you call BeginReceive(). Use it only while processing the result from EndReceive().

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136