1

I'm trying to understand why I get ArgumentNull exception on Monitor.Enter method. The calling code only uses a single instance of NamedLocker. I'm using a lock to protect from accessing the shared resource. Is the semaphore being disposed causing the null reference exception?

System.ArgumentNullException: Value cannot be null.
at System.Threading.Monitor.Enter(Object obj)
at System.Threading.SemaphoreSlim.<WaitUntilCountOrTimeoutAsync>d__31.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()

The exception seems to be coming when doing a WaitAsync after getting the Semaphore.
Following is my code:

public class NamedLocker
{
    private static readonly Dictionary<string, SemaphoreSlim> _lockDict =
        new Dictionary<string, SemaphoreSlim>();
    private readonly Config config;
    private static readonly object semLock = new object();
    public NamedLocker(Config config)
    {
        this.config = config;
    }

    public async Task RunWithLock(string name, Func<Task> body)
    {
        try
        {
            _ = await GetOrCreateSemaphore(name)
               .WaitAsync(TimeSpan.FromSeconds(5))
               .ConfigureAwait(false);
            await body().ConfigureAwait(false);
        }
        finally
        {
            _ = Task.Run(async () =>
            {
                await Task.Delay(this.config.LockDelayMilliseconds)
                    .ConfigureAwait(false);
                RemoveLock(name);
            });
        }
    }

    public void RemoveLock(string key)
    {
        SemaphoreSlim outSemaphoreSlim;
        lock (semLock)
        {
            if (_lockDict.TryGetValue(key, out outSemaphoreSlim))
            {
                _lockDict.Remove(key);
                outSemaphoreSlim.Release();
                outSemaphoreSlim.Dispose();
            }
        }
    }

    private SemaphoreSlim GetOrCreateSemaphore(string key)
    {
        SemaphoreSlim outSemaphoreSlim;
        lock (semLock)
        {
            if (!_lockDict.TryGetValue(key, out outSemaphoreSlim))
            {
                outSemaphoreSlim = new SemaphoreSlim(1, 1);
                _lockDict[key] = outSemaphoreSlim;
            }
            return outSemaphoreSlim;
        }
    }
}

This is the test I used to validate the exception:

[TestMethod]
public async Task PPP()
{
    var namedLocker = new NamedLocker(new Config
    {
        LockDelayMilliseconds = 100
    });

    Func<Task> funcToRunInsideLock = async () => await Task.Delay(300);
    var tasks = Enumerable.Range(0, 100).Select(
        i => namedLocker.RunWithLock((i % 2).ToString(), funcToRunInsideLock));
    await Task.WhenAll(tasks).ConfigureAwait(false);
}

Edit: Removing the Dispose() in finally block (inside RemoveLock) makes it work without the null exception. I don't understand why this would be case.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
vimapzz
  • 11
  • 3
  • Are you sure this is all the code for the class `NamedLocker`? – Charlieface Dec 31 '20 at 00:46
  • Yup. This is the entire class. – vimapzz Dec 31 '20 at 00:50
  • If you are trying to make a keyed semaphore, I suggest to take a look at this question: [Asynchronous locking based on a key](https://stackoverflow.com/questions/31138179/asynchronous-locking-based-on-a-key/). Making one is not so trivial. You can't just remove the `SemaphoreSlim`s from the dictionary like that. You need to maintain a reference counter of each individual `SemaphoreSlim`. – Theodor Zoulias Dec 31 '20 at 01:39
  • 1
    @TheodorZoulias thankyou! that post was very insightful. Wrapping this implementation with Ref counting solved the problem. – vimapzz Dec 31 '20 at 02:03

0 Answers0