Предотвращение ввода двух потоков в кодовый блок с одинаковым значением
Скажем, у меня есть эта функция (предположим, что я получаю доступ к кэшу потокобезопасным способом):
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];
}