Предотвращение ввода двух потоков в кодовый блок с одинаковым значением

Скажем, у меня есть эта функция (предположим, что я получаю доступ к кэшу потокобезопасным способом):

object GetCachedValue(string id)
{
    if (!Cache.ContainsKey(id))
    {
         //long running operation to fetch the value for id
         object value = GetTheValueForId(id);
         Cache.Add(id, value);
    }     
    return Cache[id];
}

Я хочу, чтобы два потока не выполняли "длительную операцию" одновременно для одного и того же значения. Очевидно, я могу обернуть все это в lock(), но тогда вся функция блокирует независимо от значения, и я хочу, чтобы два потока могли выполнять длительную операцию, пока они ищут разные идентификаторы.

Есть ли встроенный механизм блокировки для блокировки на основе значения, чтобы один поток мог блокировать, в то время как другой поток завершает длительную операцию, поэтому мне не нужно делать это дважды (или N раз)? В идеале, если длительная работа выполняется в одном потоке, ни один другой поток не сможет сделать это для одного и того же значения id.

Я мог бы свернуть свой собственный, поместив идентификатор в HashSet, а затем удалив их после завершения операции, но это похоже на взлом.

Ответ 1

Я бы использовал Lazy<T> здесь. Ниже код заблокирует кеш, поместите Lazy в кеш и немедленно вернется. Длительная работа будет выполняться один раз в потоковом безопасном режиме.

new Thread(() => Console.WriteLine("1-" + GetCachedValue("1").Value)).Start();
new Thread(() => Console.WriteLine("2-" + GetCachedValue("1").Value)).Start();

Lazy<object> GetCachedValue(string id)
{
    lock (Cache)
    {
        if (!Cache.ContainsKey(id))
        {
            Lazy<object> lazy = new Lazy<object>(() =>
                {
                    Console.WriteLine("**Long Running Job**");
                    Thread.Sleep(3000);
                    return int.Parse(id);
                }, 
                true);

            Cache.Add(id, lazy);
            Console.WriteLine("added to cache");
        }
        return Cache[id];
    }
}

Ответ 2

Переместите блокировку вниз туда, где находится ваш комментарий. Я думаю, вам нужно поддерживать список выполняемых в настоящее время длительных операций и блокировать доступ к этому списку и выполнять только GetValueForId, если id, который вы ищете, отсутствует в этом списке. Я попробую что-нибудь взломать.

private List<string> m_runningCacheIds = new List<string>();

object GetCachedValue(string id)
{
    if (!Cache.ContainsKey(id))
    {
         lock (m_runningCacheIds) {
             if (m_runningCacheIds.Contains(id)) {
                 // Do something to wait until the other Get is done....
             }
             else {
                 m_runningCacheIds.Add(id);
             }
         }

         //long running operation to fetch the value for id

         object value = GetTheValueForId(id);
         Cache.Add(id, value);

         lock (m_runningCacheIds)
             m_runningCacheIds.Remove(id);
    }     
    return Cache[id];
}

Проблема с тем, что поток будет делать, пока он ждет другого потока, получает значение.

Ответ 3

В этих случаях я использую Mutex как:

object GetCachedValue(string Key)
{
    // note here that I use the key as the name of the mutex
    // also here you need to check that the key have no invalid charater
    //   to used as mutex name.
    var mut = new Mutex(true, key);

    try
    {   
        // Wait until it is safe to enter.
        mut.WaitOne();

        // here you create your cache
        if (!Cache.ContainsKey(Key))
        {
             //long running operation to fetch the value for id
             object value = GetTheValueForId(Key);
             Cache.Add(Key, value);
        }     

        return Cache[Key];        
    }
    finally
    {
        // Release the Mutex.
        mut.ReleaseMutex();
    }   
}

Примечания:

  • Некоторые символы недопустимы для имени мьютекса (например, косой чертой)
  • Если кеш отличается для каждого используемого приложения (или веб-пула), и если мы говорим о кеше asp.net, то мьютекс блокирует все потоки и пулы на компьютере, в этом случае Я также использую статическое случайное целое, которое я добавляю к ключу, и не делаю блокировку разной для каждого ключа, а также для каждого пула.

Ответ 4

В этом случае я хотел бы иметь такой интерфейс

using (SyncDispatcher.Enter(id))
{
    //any code here...
}

чтобы я мог выполнить любой код, и он будет потокобезопасным, если id тот же. Если мне нужно получить значение из кеша, я получаю его прямо вперед, так как нет вызовов concurrency.

Моя реализация для SyncDispatcher такова:

public class SyncDispatcher : IDisposable
{
    private static object _lock = new object();
    private static Dictionary<object, SyncDispatcher> _container = new Dictionary<object, SyncDispatcher>();

    private AutoResetEvent _syncEvent = new AutoResetEvent(true);

    private SyncDispatcher() { }

    private void Lock()
    {
        _syncEvent.WaitOne();
    }

    public void Dispose()
    {
        _syncEvent.Set();
    }

    public static SyncDispatcher Enter(object obj)
    {
        var objDispatcher = GetSyncDispatcher(obj);
        objDispatcher.Lock();

        return objDispatcher;
    }

    private static SyncDispatcher GetSyncDispatcher(object obj)
    {
        lock (_lock)
        {
            if (!_container.ContainsKey(obj))
            {
                _container.Add(obj, new SyncDispatcher());
            }

            return _container[obj];
        }
    }
}

Простой тест:

static void Main(string[] args)
{
    new Thread(() => Execute("1", 1000, "Resource 1")).Start();
    new Thread(() => Execute("2", 200, "Resource 2")).Start();
    new Thread(() => Execute("1", 0, "Resource 1 again")).Start();  
}

static void Execute(object id, int timeout, string message)
{
    using (SyncDispatcher.Enter(id))
    {
        Thread.Sleep(timeout);

        Console.WriteLine(message);              
    }
}

enter image description here

Ответ 5

Это не самое элегантное решение в мире, но я столкнулся с этой проблемой с двойной проверкой и блокировкой:

object GetCachedValue(string id)
{
    if (!Cache.ContainsKey(id))
    {
         lock (_staticObj)
         {
            if (!Cache.ContainsKey(id))
            {
               //long running operation to fetch the value for id
               object value = GetTheValueForId(id);
               Cache.Add(id, value);
            }
         }
    }     
    return Cache[id];
}