问题
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