3

EDIT: I've updated my examples to use the https://github.com/StephenCleary/AsyncEx library. Still waiting for usable hints.

There are resources, which are identified by strings (for example files, URLs, etc.). I'm looking for a locking mechanism over the resources. I've found 2 different solutions, but each has its problems:

The first is using the ConcurrentDictionary class with AsyncLock:

using Nito.AsyncEx;
using System.Collections.Concurrent;

internal static class Locking {
    private static ConcurrentDictionary<string, AsyncLock> mutexes
        = new ConcurrentDictionary<string, AsyncLock>();

    internal static AsyncLock GetMutex(string resourceLocator) {
        return mutexes.GetOrAdd(
            resourceLocator,
            key => new AsyncLock()
        );
    }
}

Async usage:

using (await Locking.GetMutex("resource_string").LockAsync()) {
    ...
}

Synchronous usage:

using (Locking.GetMutex("resource_string").Lock()) {
    ...
}

This works safely, but the problem is that the dictionary grows larger and larger, and I don't see a thread-safe way to remove items from the dictionary when no one is waiting on a lock. (I also want to avoid global locks.)

My second solution hashes the string to a number between 0 and N - 1, and locks on these:

using Nito.AsyncEx;
using System.Collections.Concurrent;

internal static class Locking {
    private const UInt32 BUCKET_COUNT = 4096;

    private static ConcurrentDictionary<UInt32, AsyncLock> mutexes
        = new ConcurrentDictionary<UInt32, AsyncLock>();

    private static UInt32 HashStringToInt(string text) {
        return ((UInt32)text.GetHashCode()) % BUCKET_COUNT;
    }

    internal static AsyncLock GetMutex(string resourceLocator) {
        return mutexes.GetOrAdd(
            HashStringToInt(resourceLocator),
            key => new AsyncLock()
        );
    }
}

As one can see, the second solution only decreases the probability of collisions, but doesn't avoid them. My biggest fear is that it can cause deadlocks: The main strategy to avoid deadlocks is to always lock items in a specific order. But with this approach, different items can map to the same buckets in different order, like: (A->X, B->Y), (C->Y, D->X). So with this solution one cannot lock on more than one resource safely.

Is there a better solution? (I also welcome critics of the above 2 solutions.)

Crouching Kitten
  • 1,135
  • 12
  • 23
  • The `Lock`/`Unlock` API looks a bit awkward and error prone. Wouldn't you prefer an API that takes advantage of the convenient [`lock`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statement) statement? For example: `lock (mutex.Get("some_string")) {/*protected region*/}` – Theodor Zoulias Feb 01 '20 at 20:48
  • @TheodorZoulias Thanks, I was thinking of that. The pro is that, in case of an exception the lock would be automatically removed. But as a next step i'm extending this for async cases, using the AsyncAutoResetEvent from a NuGet lib. And I can't see similar stuff implemented using the `lock` statement. – Crouching Kitten Feb 01 '20 at 21:37
  • @TheodorZoulias although I could use this: https://github.com/StephenCleary/AsyncEx#asynclock So I'll move into the `lock` statement direction, thanks. – Crouching Kitten Feb 01 '20 at 21:49
  • Implementing an async disposable locker based on `SemaphoreSlim` is pretty trivial, but on the other hand you couldn't go wrong by using Stephen Cleary's well tested library! – Theodor Zoulias Feb 02 '20 at 00:37

0 Answers0