Keyed Lock in .Net

后端 未结 3 2012
长发绾君心
长发绾君心 2021-01-21 20:02

I have a Azure Service Bus queue where I\'m receiving a range from 1 to 10 messages with the same \"key\". One of these messages needs to be processed with a long running operat

相关标签:
3条回答
  • 2021-01-21 20:34

    You're nearly there... do you need the incoming order preserved? If not:

    public static void Main(string[] args)
    {
        Enumerable.Range(1, 1000)
                    .AsParallel()
                    .ForAll( i => ManageConcurrency(i % 2,  () => Task.Delay(TimeSpan.FromSeconds(10))).Wait());
    
    
    }
    
    private static readonly ConcurrentDictionary<int, SemaphoreSlim> _lockDict = new ConcurrentDictionary<int, SemaphoreSlim>();
    
    private static async Task<bool> ManageConcurrency(int taskId, Func<Task> task)
    {
    
        var gate = _lockDict.GetOrAdd(taskId, _ => new SemaphoreSlim(1, 1));
        await gate.WaitAsync();
    
        try
        {
    
            Console.WriteLine($"{DateTime.Now.ToString("hh:mm:ss.ffffff")},  {taskId}, Lock pulled for TaskId {taskId}, Thread Id: {System.Threading.Thread.CurrentThread.ManagedThreadId}");
    
            await task();
    
            return true;
        }
        catch (Exception e)
        {
            return false;
        }
        finally
        {
            gate.Release();
        }
    
    }
    
    0 讨论(0)
  • 2021-01-21 20:39

    You correctly recognized that you need to ensure that only one semaphore is being created per key. The standard idiom for that is:

    var dict = new ConcurrentDictionary<TKey, Lazy<SemaphoreSlim>>();
    ...
    var sem = dict.GetOrAdd( , _ => new new Lazy<SemaphoreSlim>(() => SemaphoreSlim(1, 1))).Value;
    

    Multiple lazies might be created but only one of them will ever be revealed and materialized.

    Besides that it is a questionable practice to rely on in-memory state. What if your queue processing app recycles and all semaphores are lost? You better use a persistent store to track this locking info.

    0 讨论(0)
  • 2021-01-21 20:40

    It seems to me that you're making your life harder with worrying about semaphores and the like. There are easier abstractions to use.

    Using Lazy<T> is ideal in this situation, but since you want to await the results then Lazy<T> needs an upgrade to AsyncLazy<T>.

    public class AsyncLazy<T> : Lazy<Task<T>>
    {
        public AsyncLazy(Func<T> valueFactory) :
            base(() => Task.Factory.StartNew(valueFactory))
        { }
    
        public AsyncLazy(Func<T> valueFactory, LazyThreadSafetyMode mode) :
            base(() => Task.Factory.StartNew(valueFactory), mode)
        { }
    
        public AsyncLazy(Func<Task<T>> taskFactory) :
            base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap())
        { }
    
        public AsyncLazy(Func<Task<T>> taskFactory, LazyThreadSafetyMode mode) :
            base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap(), mode)
        { }
    
        public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
    }
    

    I've created a class to simulate the result of the long running task:

    public class LongRunningResult
    {
        public int Index;
    }
    

    And the method that needs to get run to do the computation:

    private LongRunningResult ComputeLongRunningResult(int index)
    {
        Console.WriteLine($"Running Index {index}");
        Thread.Sleep(1000);
        return new LongRunningResult() { Index = index };
    }
    

    Now we need the dictionary to hold the lazy asyncs:

    private readonly ConcurrentDictionary<int, AsyncLazy<LongRunningResult>> _results
        = new ConcurrentDictionary<int, AsyncLazy<LongRunningResult>>();
    

    Now it becomes super easy:

    Enumerable
        .Range(1, 10)
        .AsParallel()
        .ForAll(async i =>
        {
            var index = i % 2;
            Console.WriteLine($"Trying Index {index}");
            _results.TryAdd(index,
                new AsyncLazy<LongRunningResult>(
                    () => ComputeLongRunningResult(index),
                    LazyThreadSafetyMode.ExecutionAndPublication));
            AsyncLazy<LongRunningResult> ayncLazy;
            if (_results.TryGetValue(index, out ayncLazy))
            {
                await ayncLazy;
            }
        });
    

    The output I get from this is like:

    Trying Index 1
    Trying Index 0
    Trying Index 1
    Trying Index 1
    Trying Index 0
    Trying Index 1
    Running Index 1
    Trying Index 0
    Trying Index 1
    Running Index 0
    Trying Index 0
    Trying Index 0
    
    0 讨论(0)
提交回复
热议问题