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. Commented Apr 30, 2024 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) } 

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.