16

I am working on a C# API that is used by a variety of consumers. This API provides access to a shared resource (in my case hardware that does serial communication) , that will often have a few different actors trying to use it concurrently.

The issue I have is that some of my consumers will want to use this in a multi-threaded environment - each actor works independently and try to use the resource. A simple lock works fine here. But some of my consumers would prefer to use async-await and time-slice the resource. (As I understand it) this requires an asynchronous lock to yield the timeslice back to other tasks; blocking at a lock would halt that whole thread.

And I imagine that having serial locks is unperformant at best, and a potential race condition or deadlock at worst.

So how can I protect this shared resource in a common codebase for both potential concurrency usages?

i3arnon
  • 113,022
  • 33
  • 324
  • 344
Telastyn
  • 508
  • 1
  • 5
  • 11

1 Answers1

41

You can use SemaphoreSlim with 1 as the number of requests. SemaphoreSlim allows to lock in both an async fashion using WaitAsync and the old synchronous way:

await _semphore.WaitAsync()
try
{
    ... use shared resource.
}
finally
{
    _semphore.Release()
}

You can also write your own AsyncLock based on Stephen Toub's great post Building Async Coordination Primitives, Part 6: AsyncLock. I did it in my application and allowed for both synchronous and asynchronous locks on the same construct.

Usage:

// Async
using (await _asyncLock.LockAsync())
{
    ... use shared resource.
}

// Synchronous
using (_asyncLock.Lock())
{
    ... use shared resource.
}

Implementation:

class AsyncLock
{
    private readonly Task<IDisposable> _releaserTask;
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    private readonly IDisposable _releaser;

    public AsyncLock()
    {
        _releaser = new Releaser(_semaphore);
        _releaserTask = Task.FromResult(_releaser);
    }
    public IDisposable Lock()
    {
        _semaphore.Wait();
        return _releaser;
    }
    public Task<IDisposable> LockAsync()
    {
        var waitTask = _semaphore.WaitAsync();
        return waitTask.IsCompleted
            ? _releaserTask
            : waitTask.ContinueWith(
                (_, releaser) => (IDisposable) releaser,
                _releaser,
                CancellationToken.None,
                TaskContinuationOptions.ExecuteSynchronously,
                TaskScheduler.Default);
    }
    private class Releaser : IDisposable
    {
        private readonly SemaphoreSlim _semaphore;
        public Releaser(SemaphoreSlim semaphore)
        {
            _semaphore = semaphore;
        }
        public void Dispose()
        {
            _semaphore.Release();
        }
    }
}
Martijn Wijns
  • 501
  • 7
  • 19
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • This doesn't look to solve my question. This provides the same mechanism, but would still force my API to choose between async and synchronous locks - when it necessarily cannot know which of the two is calling it. Unless I misunderstand? – Telastyn Jan 09 '14 at 12:20
  • 1
    @Telastyn: Your API methods are either synchronous or asynchronous. If they're synchronous, use `Wait`; if they're asynchronous, use `WaitAsync`. – Stephen Cleary Jan 09 '14 at 13:47
  • @StephenCleary - the shared resource is ~4 layers deep under the hood. At that point in the code it doesn't (and _shouldn't_ know) know if it's being called asyncronously or not. Plumbing a parallel path through the codebase is not only impractical, but seems unnecessary. This certainly seems like a problem others have encountered? – Telastyn Jan 09 '14 at 13:59
  • 3
    @Telastyn: The proper way to do `async` is to do `async` "all the way". Yes, that means if you are sure you want both async and sync, then you do end up with parallel paths. There are hacks to do async over sync or sync over async, but none of them work perfectly or work in every situation. – Stephen Cleary Jan 09 '14 at 14:11
  • @StephenCleary - hrmf, that makes it decidedly less usable. – Telastyn Jan 09 '14 at 14:23
  • 1
    Okay, I did some experiments and it looks as though the async lock should do what I need since it will protect the shared resource even in the multi-threaded scenario. The continuation (appears) to simply run in that separate thread, providing the correct behavior. And the intermediary layers between the resource and the API boundary can appear not-async. – Telastyn Jan 09 '14 at 14:35
  • I'm glad this works for you. Sorry for not being responsive, but i guess you were better off in the trusted hands of @StephenCleary – i3arnon Jan 09 '14 at 15:16
  • 3
    Adding to what @StephenCleary said, interweaving sync and async down a call stack can quickly cause an effective deadlock. Many times async tasks get scheduled on the same thread, and if there is a synchronous blocking call mixed in between two async calls, the parent async call may block indefinitely, waiting on the child async call that can never complete because of the synchronous blocking call. Because they both run on the same thread, all work stops. Or something like that. I had this happen once and I vowed to never do it again. – Bengie Jun 04 '14 at 18:16