0

So I'm working on the Web API for my website and certain API calls need to be performed with thread safety in the application's runtime. I have created a locking service which uses a semaphore for locking. The locking service has been declared as a singleton dependency. This locking service is to be used within my API controller with the following flow: controller waits to obtain lock -> controller obtains lock -> perform thread sensitive process -> release lock

Below is the code for my locking service:

public class LockingService : ILockingService
{
    private SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public LockingService() { }

    public async Task<bool> WaitForLock()
    {
        bool result = false;
        await _semaphore.WaitAsync();
        result = true;
        return result;
    }

    public void ReleaseLock()
    {
        _semaphore.Release(1);
    }
}

Program.cs:

builder.Services.AddSingleton<ILockingService,LockingService>();

Usage in controller:

public async Task MyMethod()
{
    await _lockingService.WaitForLock();
    
    // Perform thread sensitive process

    _lockingService.ReleaseLock();
}

From what I currently understand, singleton dependencies are shared as a single instance throughout the application's lifetime. Am I correct to infer that this implementation is thread safe?

1
  • 1
    This is thread safe, while simultaneously belonging to stackoverflow.
    – Ccm
    Commented Apr 30 at 7:45

1 Answer 1

2

This is thread safe, since the controllers always get the same instance of the LockingService.

While such a "globally" accessible service is not exactly clean, it can be a pragmatic solution. However, the current code is error-prone, because other services must not forget to call ReleaseLock in all cases (especially exceptions). It can help to replace the WaitForLock and ReleaseLock with something like RunWithLock that takes a callback:

public class LockingService : ILockingService
{
    private SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public async Task RunWithLockAsync(Func<Task> callback)
    {
        try
        {
            await _semaphore.WaitAsync();
            await callback.Invoke();
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

This guarantees that the semaphore will always be released when the callback is finished.

Usage:

public async Task MyMethod()
{
    await _lockingService.RunWithLockAsync(() =>
    {
        // do something while holding the lock
    });
}

If you have operations that need to compute a result while holding the lock, you can add a second overload:

public async Task<T> RunWithLockAsync<T>(Func<Task<T>> callback)
{
    try
    {
        await _semaphore.WaitAsync();
        return await callback.Invoke();
    }
    finally
    {
        _semaphore.Release();
    }
}

Usage:

public async Task MyMethod()
{
    var result = await _lockingService.RunWithLockAsync(() =>
    {
        // compute something while holding the lock
        return 42;
    });

    // do something with the result (without holding the lock)
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.