Locking with nested async calls

前端 未结 4 1503
轮回少年
轮回少年 2021-02-13 11:50

I am working on a multi threaded WindowsPhone8 app that has critical sections within async methods.

Does anyone know of a way to properly use semaphores / mutexes in C

相关标签:
4条回答
  • 2021-02-13 12:27

    You can use the System.Threading.ReaderWriterLockSlim (doc), which has a support recursion flag:

    ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
    
    async Task Bar()
    {
        try
        {
            _lock.EnterReadLock();
            await BarInternal();
        }
        finally
        {
            if (_lock.IsReadLockHeld)
                _lock.ExitReadLock();
        }
    }
    
    async Task BarInternal()
    {
        try
        {
            _lock.EnterReadLock();
            await Task.Delay(1000);
        }
        finally
        {
            if (_lock.IsReadLockHeld)
                _lock.ExitReadLock();
        }
    }
    

    Still you should be very careful with recursion because it is very difficult to control which thread took a lock and when.

    The code in the question will be result in a deadlock because it tries to acquire the lock twice, something like:

    await _lock.WaitAsync();
    await _lock.WaitAsync(); --> Will result in exception.
    

    While flagging the ReaderWriterLockSlim in SupportsRecursion will not throw an exception for this weird code:

     _lock.EnterReadLock();
     _lock.EnterReadLock();
    
    0 讨论(0)
  • 2021-02-13 12:28

    Here's what I did in such a situation (still, I'm not experienced with tasks, so don't beat me ;-)
    So basically you have move the actual implementation to non locking methods and use these in all methods which acquire a lock.

    public class Foo
    {
        SemaphoreSlim _lock = new SemaphoreSlim(1);
    
        public async Task Bar()
        {
            await _lock.WaitAsync();
            await BarNoLock();
            _lock.Release();
         }
    
        public async Task BarInternal()
        {
            await _lock.WaitAsync(); // no deadlock
            await BarNoLock();
            _lock.Release();
         }
    
         private async Task BarNoLock()
         {
             // do the work
         }
    }
    
    0 讨论(0)
  • 2021-02-13 12:33

    Recursive locks are a really bad idea (IMO; link is to my own blog). This is especially true for async code. It's wicked difficult to get async-compatible recursive locks working. I have a proof-of-concept here but fair warning: I do not recommend using this code in production, this code will not be rolled into AsyncEx, and it is not thoroughly tested.

    What you should do instead is restructure your code as @svick stated. Something like this:

    public async Task Bar()
    {
        await _lock.WaitAsync();
    
        await BarInternal_UnderLock();
    
        _lock.Release();
    }
    
    public async Task BarInternal()
    {
        await _lock.WaitAsync();
    
        await BarInternal_UnderLock();
    
        _lock.Release();
    }
    
    private async Task BarInternal_UnderLock()
    {
        // DO work
    }
    
    0 讨论(0)
  • 2021-02-13 12:46

    First, read through Stephen Cleary's blog post, which he linked to in his answer. He mentions multiple reasons, such as uncertain lock state and inconsistent invariants, which are associated with recursive locks (not to mention recursive async locks). If you can do the refactoring he and Knickedi describe in their answers, that would be great.

    However, there are some cases where that type of refactoring is just not possible. Fortunately, there are now multiple libraries which support nested async calls (lock reentrance). Here are two. The author of the first has a blog post where he talks more about it.

    • https://github.com/neosmart/AsyncLock
    • https://github.com/mysteryjeans/Flettu/

    You can incorporate it into your code as such (using the first library in this example):

    public class Foo
    {
        AsyncLock _lock = new AsyncLock();
    
        public async Task Bar()
        {
               // This first LockAsync() call should not block
               using (await _lock.LockAsync())
               {
                   await BarInternal();
               }
         }
    
        public async Task BarInternal()
        {
               // This second call to LockAsync() will be recognized
               // as being a reëntrant call and go through
               using (await _lock.LockAsync()) // no deadlock
               {
                   // do work
               }
         }
    }
    
    0 讨论(0)
提交回复
热议问题