Prevent two threads entering a code block with the same value

不问归期 提交于 2019-12-20 09:40:00

问题


Say I have this function (assume I'm accessing Cache in a threadsafe way):

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];
}

I want to prevent two threads from running the "long running operation" at the same time for the same value. Obviously I can wrap the whole thing in a lock(), but then the whole function would block regardless of value and I want two threads to be able to perform the long running operation as long as they're looking for different id's.

Is there a built-in locking mechanism to lock based on a value so one thread can block while the other thread completes the long running operation so I don't need to do it twice (or N times)? Ideally as long as the long running operation is being performed in one thread, no other thread should be able to do it for the same id value.

I could roll my own by putting the id's in a HashSet and then removing them once the operation completes, but that seems like a hack.


回答1:


I would use Lazy<T> here. Below code will lock the cache, put the Lazy into the cache and return immediately. Long-running-operation will be executed once in a thread safe manner.

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:


In this case I would like to have interface like this

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

so I could execute any code and it would be thread safe if id is the same. If I need to get value from Cache I get it straight forward, as just there is no concurrency calls.

My implementation for SyncDispatcher is this:

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];
        }
    }
}

Simple test:

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);              
    }
}




回答3:


Move your locking down to where your comment is. I think you need to maintain a list of currently-executing long running operations, and lock accesses to that list, and only execute the GetValueForId if the id you're looking for isn't in that list. I'll try and whip something up.

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];
}

There's still the issue of what the thread is going to do while it waits on the other thread is getting the value.




回答4:


I use in that cases the Mutex as:

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();
    }   
}

Notes:

  • Some characters are not valid for the mutex name (like the slash)
  • If the Cache is different for every application (or web pool) that you use, and it is if we speak for the cache of asp.net, then the mutex is lock all threads and pools in the computer, in this case I also use a static random integer that I add it to the key, and not make the lock different for each key but also for each pool.



回答5:


It's not the most elegant solution in the world, but I've gotten around this issue with a double check and a lock:

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];
}


来源:https://stackoverflow.com/questions/14073147/prevent-two-threads-entering-a-code-block-with-the-same-value

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!