Search code examples
c#.net-corethread-safetylockingmediator

How to add thread lock to a transient service


I am using Mediator in .Net Core 5 and want to add a lock while processing a task.

I have used the below codes, but it does not work as the Mediator handlers are not Singleton services.

public class MyCommandHandler : IRequestHandler<MyCommand, bool>
{
  private object thisLock = new();
  private readonly IService _service;

  public MyCommandHandler(IService service)
  {
      _service = service;
  }

  public async Task<bool> Handle(MyCommand request, CancellationToken cancellationToken)
  {
     var items = _service.Get().Result;
     lock (thisLock)
     {
        //Some work
        await _service.Add(new Foo{ Id = 1 });   
     }
  }
}

In my Startup

services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddSingleton<IService , MyService >();

Is there a way I can fix this locking?

I have tried to register the handler as a Singleton service but it gave me a run time error

services.AddSingleton(typeof(IRequestHandler<,>), typeof(IRequestHandler<MyCommand, bool>));

Solution

  • Make your lock static and it will be shared across all instances of MyCommandHandler.

    private static object thisLock = new();
    

    You can see the difference with a simple dummy app:

    public class StaticLock
    {
        private static object _lock = new();
        
        public void Increment(ref int counter)
        {
            lock (_lock)
            {
                counter++;
            }
        }
    }
    
    public class InstanceLock
    {
        private object _lock = new();
    
        public void Increment(ref int counter)
        {
            lock (_lock)
            {
                counter++;
            }
        }
    }
    

    which we can test with the following:

    var counter = 0;
    var tasks = new List<Task>();
    
    for (int i = 0; i < 1000; i++)
    {
        tasks.Add(Task.Run(() => new StaticLock().Increment(ref counter)));
    }
    
    Task.WhenAll(tasks).GetAwaiter().GetResult();
    Console.WriteLine(counter);
    
    var counter2 = 0;
    var tasks2 = new List<Task>();
    
    for (int i = 0; i < 1000; i++)
    {
        tasks2.Add(Task.Run(() => new InstanceLock().Increment(ref counter2)));
    }
    
    Task.WhenAll(tasks2).GetAwaiter().GetResult();
    Console.WriteLine(counter2);
    

    When using a static lock, we always increment the count to 1000, since every increment is using the shared lock.

    With the instance lock, you will lose several of the increments since they will overwrite the competing updates and will only lock for that specific instance.