Impedire che due thread inseriscano un blocco di codice con lo stesso valore

Supponiamo che abbia questa funzione (supponiamo che acceda a Cache in un modo sicuro):

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

Voglio impedire a due thread di eseguire ” l’operazione long run ” allo stesso tempo per lo stesso valore . Ovviamente posso racchiudere il tutto in un lock (), ma poi l’intera funzione si bloccherebbe indipendentemente dal valore e voglio che due thread siano in grado di eseguire l’operazione long running purché stiano cercando id diversi.

C’è un meccanismo di blocco incorporato da bloccare in base a un valore in modo che un thread possa bloccarsi mentre l’altro thread completa l’ operazione a lungo termine, quindi non è necessario farlo due volte (o N volte)? Idealmente, finché l’ operazione di lunga durata viene eseguita in un thread, nessun altro thread dovrebbe essere in grado di farlo per lo stesso valore id.

Potrei girare da solo inserendo gli id ​​in un HashSet e poi rimuovendoli una volta che l’operazione è stata completata, ma sembra un trucco.

Vorrei usare Lazy qui. Sotto il codice bloccherà la cache, metterà il Lazy nella cache e ritornerà immediatamente. L’operazione a lungo termine verrà eseguita una volta in modalità thread-safe.

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

 Lazy GetCachedValue(string id) { lock (Cache) { if (!Cache.ContainsKey(id)) { Lazy lazy = new Lazy(() => { 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]; } } 

In questo caso mi piacerebbe avere un’interfaccia come questa

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

quindi potevo eseguire qualsiasi codice e sarebbe thread sicuro se id è lo stesso. Se ho bisogno di ottenere valore da Cache, lo faccio in modo diretto, poiché non ci sono chiamate simultanee.

La mia implementazione per SyncDispatcher è questa:

 public class SyncDispatcher : IDisposable { private static object _lock = new object(); private static Dictionary _container = new Dictionary(); 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]; } } } 

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

inserisci la descrizione dell'immagine qui

Sposta il blocco verso il basso dove si trova il tuo commento. Penso che sia necessario mantenere un elenco di operazioni a esecuzione prolungata attualmente in esecuzione e bloccare gli accessi a tale elenco e eseguire solo GetValueForId se l’ id si sta cercando non è in tale elenco. Proverò a montare qualcosa.

 private List m_runningCacheIds = new List(); 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]; } 

C’è ancora il problema di cosa farà il thread mentre attende che l’altro thread stia ottenendo il valore.

Io uso in questi casi il Mutex come:

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

Gli appunti:

  • Alcuni caratteri non sono validi per il nome mutex (come la barra)
  • Se la Cache è diversa per ogni applicazione (o pool Web) che usi, e lo è se parliamo per la cache di asp.net, il mutex è bloccare tutti i thread e i pool nel computer, in questo caso uso anche un numero intero casuale statico che aggiungo alla chiave e non rende il blocco diverso per ogni chiave ma anche per ciascun pool.

Non è la soluzione più elegante al mondo, ma ho risolto questo problema con un doppio controllo e un blocco:

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