2010-11-19 16 views
28

Ho bisogno di bloccare una sezione di codice per stringa. Ovviamente il seguente codice è orribilmente pericoloso:Blocco per stringa. È sicuro/sano?

lock("http://someurl") 
{ 
    //bla 
} 

Quindi ho cucinato un'alternativa. Di solito non sono io a pubblicare grandi quantità di codice, ma quando si tratta di programmazione simultanea, sono un po 'preoccupato di creare il mio schema di sincronizzazione, quindi sto inviando il mio codice per chiedere se è sano farlo in questo modo o se c'è un approccio più diretto.

public class StringLock 
{ 
    private readonly Dictionary<string, LockObject> keyLocks = new Dictionary<string, LockObject>(); 
    private readonly object keyLocksLock = new object(); 

    public void LockOperation(string url, Action action) 
    { 
     LockObject obj; 
     lock (keyLocksLock) 
     { 
      if (!keyLocks.TryGetValue(url, 
             out obj)) 
      { 
       keyLocks[url] = obj = new LockObject(); 
      } 
      obj.Withdraw(); 
     } 
     Monitor.Enter(obj); 
     try 
     { 
      action(); 
     } 
     finally 
     { 
      lock (keyLocksLock) 
      { 
       if (obj.Return()) 
       { 
        keyLocks.Remove(url); 
       } 
       Monitor.Exit(obj); 
      } 
     } 
    } 

    private class LockObject 
    { 
     private int leaseCount; 

     public void Withdraw() 
     { 
      Interlocked.Increment(ref leaseCount); 
     } 

     public bool Return() 
     { 
      return Interlocked.Decrement(ref leaseCount) == 0; 
     } 
    } 
} 

vorrei utilizzare in questo modo:

StringLock.LockOperation("http://someurl",()=>{ 
    //bla 
}); 

bene ad andare, o crash e bruciare?

EDIT

Ai posteri, ecco il mio codice di lavoro. Grazie per tutti i suggerimenti:

public class StringLock 
{ 
    private readonly Dictionary<string, LockObject> keyLocks = new Dictionary<string, LockObject>(); 
    private readonly object keyLocksLock = new object(); 

    public IDisposable AcquireLock(string key) 
    { 
     LockObject obj; 
     lock (keyLocksLock) 
     { 
      if (!keyLocks.TryGetValue(key, 
             out obj)) 
      { 
       keyLocks[key] = obj = new LockObject(key); 
      } 
      obj.Withdraw(); 
     } 
     Monitor.Enter(obj); 
     return new DisposableToken(this, 
            obj); 
    } 

    private void ReturnLock(DisposableToken disposableLock) 
    { 
     var obj = disposableLock.LockObject; 
     lock (keyLocksLock) 
     { 
      if (obj.Return()) 
      { 
       keyLocks.Remove(obj.Key); 
      } 
      Monitor.Exit(obj); 
     } 
    } 

    private class DisposableToken : IDisposable 
    { 
     private readonly LockObject lockObject; 
     private readonly StringLock stringLock; 
     private bool disposed; 

     public DisposableToken(StringLock stringLock, LockObject lockObject) 
     { 
      this.stringLock = stringLock; 
      this.lockObject = lockObject; 
     } 

     public LockObject LockObject 
     { 
      get 
      { 
       return lockObject; 
      } 
     } 

     public void Dispose() 
     { 
      Dispose(true); 
      GC.SuppressFinalize(this); 
     } 

     ~DisposableToken() 
     { 
      Dispose(false); 
     } 

     private void Dispose(bool disposing) 
     { 
      if (disposing && !disposed) 
      { 
       stringLock.ReturnLock(this); 
       disposed = true; 
      } 
     } 
    } 

    private class LockObject 
    { 
     private readonly string key; 
     private int leaseCount; 

     public LockObject(string key) 
     { 
      this.key = key; 
     } 

     public string Key 
     { 
      get 
      { 
       return key; 
      } 
     } 

     public void Withdraw() 
     { 
      Interlocked.Increment(ref leaseCount); 
     } 

     public bool Return() 
     { 
      return Interlocked.Decrement(ref leaseCount) == 0; 
     } 
    } 
} 

utilizzati come segue:

var stringLock=new StringLock(); 
//... 
using(stringLock.AcquireLock(someKey)) 
{ 
    //bla 
} 
+2

Sembra troppo ingegnoso per me, ma difficile da dire senza sapere quale problema dovrebbe risolvere. È giusto per evitare di bloccarsi su una corda? – LukeH

+0

@LukeH, Sto cercando di scrivere una cache di immagini in cui più client (più di 100) richiedono l'immagine simultaneamente. Il primo hit richiederà l'immagine da un altro server e mi piacerebbe che tutte le altre richieste alla cache venissero bloccate fino a quando l'operazione non sarà completa, in modo che possano recuperare la versione cache. A mio avviso, ciò richiederà il blocco di questa natura, ma se ci sono alternative, sono tutto orecchie. – spender

+0

@Ani, sì, sono a conoscenza di questo (come descritto nel mio post) e se considero questo approccio come un frequentatore, farò da barriera alle difese. – spender

risposta

10

Un'altra opzione è ottenere il codice hash di ciascun URL, quindi dividerlo per un numero primo e utilizzarlo come indice in una serie di blocchi. Questo limiterà il numero di serrature di cui hai bisogno mentre ti permetterò di controllare la probabilità di un "false locking" scegliendo il numero di blocchi da usare.

Tuttavia, quanto sopra è utile solo se è troppo costoso avere un solo blocco per URL attivo.

+2

Il grande vantaggio di questo codice è che è abbastanza semplice da implementare poiché, a differenza del codice basato sul dizionario, non ha bisogno di rimuovere gli oggetti di blocco per evitare una perdita di memoria. Questo è l'unico codice in questo argomento che sarei sicuro di implementare correttamente senza pensarci troppo. E ovviamente tenere più di un lucchetto alla volta in un thread diventa davvero brutto. – CodesInChaos

+0

Grazie amico! So che questo è come 4 anni più tardi, ma mi hai appena aiutato a trovare un'ottima soluzione per un problema che sto avendo. :) – Haney

+0

@Ian Ringrose puoi fornire un esempio di codice? –

14

Blocco da un'istanza stringa arbitraria sarebbe una cattiva idea, perché Monitor.Lock blocca l'istanza. Se avessi due diverse istanze di stringa con lo stesso contenuto, sarebbero due blocchi indipendenti, che non vuoi. Quindi hai ragione a preoccuparti delle stringhe arbitrarie.

Tuttavia, .NET ha già un meccanismo incorporato per restituire "l'istanza canonica" del contenuto di una determinata stringa: String.Intern. Se si passano due diverse istanze di stringa con lo stesso contenuto, si otterrà sempre la stessa istanza di risultato.

lock (string.Intern(url)) { 
    ... 
} 

Questo è più semplice; c'è meno codice da testare, perché ci si affida a ciò che è già nel Framework (che, presumibilmente, funziona già).

+1

Monitor.Enter non blocca l'istanza. L'istanza è solo un token utilizzato per implementare il protocollo di sincronizzazione. –

+0

Questo è * precisamente * ciò che l'OP sta tentando di evitare. Se un altro thread stava eseguendo il codice da un modulo diverso che ha deciso di bloccare la stessa stringa internata (per coincidenza), ci sarebbe una contesa completamente inutile. L'OP sta cercando di fornire all'utente la facilità del blocco delle stringhe, ma senza che il pool interno .NET si intrometta. – Ani

+3

E memoria interna di perdite – CodesInChaos

2

Circa 2 anni fa ho dovuto implementare la stessa cosa:

Sto cercando di scrivere una cache immagine in cui più client (oltre 100 o così) possono richiedere l'immagine contemporaneamente. Il primo hit sarà richiedere l'immagine da un altro server, e vorrei che tutte le altre richieste alla cache bloccassero fino a quando questa operazione è completa in modo che possano recuperare la versione cache memorizzata nella cache.

Mi sono ritrovato con il codice che si comportava esattamente come il tuo (il dizionario di LockObjects). Bene, il tuo sembra essere meglio incapsulato.

Quindi, penso che tu abbia una buona soluzione. A soli 2 commenti:

  1. Se avete bisogno di ancora migliore peformance è forse utile utilizzare un qualche tipo di ReadWriteLock, dal momento che si dispone di 100 lettori e solo 1 scrittore ottenere l'immagine da un altro server.

  2. Non sono sicuro di cosa succederebbe nel caso dell'interruttore di filettatura appena prima di Monitor.Enter (obj); nel tuo LockOperation(). Supponiamo che il primo thread che desidera l'immagine costruisca un nuovo blocco e quindi il thread switch appena prima che entri nella sezione critica. Quindi potrebbe accadere che il secondo thread entri nella sezione critica prima del primo. Beh, potrebbe essere che questo non è un vero problema.

+0

Ciò significa che il mio secondo punto è rilevante se ci si basa sul fatto che il primo thread che entra nella sezione critica è lo stesso che ha creato il blocco. Ad esempio, se questo è il primo thread che attiva il recupero dell'immagine da un altro server. – Adesit

2

Hai creato un involucro piuttosto complesso attorno a una semplice istruzione di blocco. Non sarebbe meglio creare un dizionario di URL e creare un oggetto di blocco per tutti e tutti. Potresti semplicemente farlo.

objLink = GetUrl("Url"); //Returns static reference to url/lock combination 
lock(objLink.LockObject){ 
    //Code goes here 
} 

Si potrebbe anche semplificare questo bloccando l'oggetto objLink wich direttamente potrebbe essere l'istanza stringa GetUrl ("URL"). (Dovresti bloccare l'elenco statico di stringhe)

Sei codice originale se molto soggetto a errori. Se il codice:

if (obj.Return()) 
{ 
keyLocks.Remove(url); 
} 

Se il codice finale originale genera un'eccezione, rimane uno stato LockObject non valido.

+0

Le uniche eccezioni che questo codice dovrebbe generare sono eccezioni asincrone come StackOverflow, OutOfMemory e abortire il thread nel qual caso dovresti scaricare comunque l'appdomain, quindi non importa molto. – CodesInChaos

-1

Nella maggior parte dei casi, quando pensi di aver bisogno di serrature, non lo fai. Invece, prova a utilizzare la struttura dati thread-safe (ad esempio la coda) che gestisce il blocco per te.

Per esempio, in pitone:

class RequestManager(Thread): 
    def __init__(self): 
     super().__init__() 
     self.requests = Queue() 
     self.cache = dict() 
    def run(self):   
     while True: 
      url, q = self.requests.get() 
      if url not in self.cache: 
       self.cache[url] = urlopen(url).read()[:100] 
      q.put(self.cache[url]) 

    # get() runs on MyThread's thread, not RequestManager's 
    def get(self, url): 
     q = Queue(1) 
     self.requests.put((url, q)) 
     return q.get() 

class MyThread(Thread): 
    def __init__(self): 
     super().__init__() 
    def run(self): 
     while True: 
      sleep(random()) 
      url = ['http://www.google.com/', 'http://www.yahoo.com', 'http://www.microsoft.com'][randrange(0, 3)] 
      img = rm.get(url) 
      print(img) 
+0

L'OP ha richiesto una soluzione C# – JJS

-1

Ora ho lo stesso problema. Ho deciso di usare una soluzione semplice: al fine di evitare situazioni di stallo creare una nuova stringa con prefisso univoco fisso e usarlo come un oggetto di blocco:

lock(string.Intern("uniqueprefix" + url)) 
{ 
// 
} 
+3

Questo è un po 'meno terribile che non garantire che sia internato, ma ancora una pessima idea. Non dovresti bloccare i dati che sono pubblicamente disponibili; dovresti bloccare i dati che sono accessibili solo ai blocchi di codice che dovrebbero essere in grado di bloccarli. – Servy

5

Ecco un semplice , soluzione molto elegante e corretto per .NET 4 Uso ConcurrentDictionary adattato da this question.

public static class StringLocker 
{ 
    private static readonly ConcurrentDictionary<string, object> _locks = new ConcurrentDictionary<string, object>(); 

    public static void DoAction(string s, Action action) 
    { 
     lock(_locks.GetOrAdd(s, new object())) 
     { 
      action(); 
     } 
    } 
} 

È possibile utilizzare questo modo:

StringLocker.DoAction("http://someurl",() => 
{ 
    ... 
}); 
+3

Questa implementazione non riattiverà mai i blocchi inutilizzati e finirà col consumare molta memoria in un lungo processo in esecuzione. – CaseyB

+0

@CaseyB il blocco verrà recuperato alla fine del blocco di codice 'lock'. Penso che tu voglia dire che gli oggetti di blocco * non usati rimarranno nel dizionario '_locks'. Questo è vero, ma l'utilizzo della memoria sarà proporzionale al numero di URL ... e se fai alcuni calcoli rapidi vedrai che il numero di URL deve essere ** estremamente ** grande perché la memoria sia un problema. –

+0

@ZaidMasud: Questa è un'idea valida, ma forse CaseyB ha un punto ... potrebbe essere risolto avvolgendo 'action();' all'interno di try-finally e rimuovendo la stringa da '_locks' nella clausola finally – digEmAll

0

ho aggiunto una soluzione in Bardock.Utils pacchetto ispirato @ eugene-beresovsky answer.

Usage:

private static LockeableObjectFactory<string> _lockeableStringFactory = 
    new LockeableObjectFactory<string>(); 

string key = ...; 

lock (_lockeableStringFactory.Get(key)) 
{ 
    ... 
} 

Solution code:

namespace Bardock.Utils.Sync 
{ 
    /// <summary> 
    /// Creates objects based on instances of TSeed that can be used to acquire an exclusive lock. 
    /// Instanciate one factory for every use case you might have. 
    /// Inspired by Eugene Beresovsky's solution: https://stackoverflow.com/a/19375402 
    /// </summary> 
    /// <typeparam name="TSeed">Type of the object you want lock on</typeparam> 
    public class LockeableObjectFactory<TSeed> 
    { 
     private readonly ConcurrentDictionary<TSeed, object> _lockeableObjects = new ConcurrentDictionary<TSeed, object>(); 

     /// <summary> 
     /// Creates or uses an existing object instance by specified seed 
     /// </summary> 
     /// <param name="seed"> 
     /// The object used to generate a new lockeable object. 
     /// The default EqualityComparer<TSeed> is used to determine if two seeds are equal. 
     /// The same object instance is returned for equal seeds, otherwise a new object is created. 
     /// </param> 
     public object Get(TSeed seed) 
     { 
      return _lockeableObjects.GetOrAdd(seed, valueFactory: x => new object()); 
     } 
    } 
} 
0

Questo è come ho implementato questo schema di bloccaggio:

public class KeyLocker<TKey> 
{ 
    private class KeyLock 
    { 
     public int Count; 
    } 

    private readonly Dictionary<TKey, KeyLock> keyLocks = new Dictionary<TKey, KeyLock>(); 

    public T ExecuteSynchronized<T>(TKey key, Func<TKey, T> function) 
    { 
     KeyLock keyLock = null; 
     try 
     {    
      lock (keyLocks) 
      { 
       if (!keyLocks.TryGetValue(key, out keyLock)) 
       { 
        keyLock = new KeyLock(); 
        keyLocks.Add(key, keyLock); 
       } 
       keyLock.Count++; 
      } 
      lock (keyLock) 
      { 
       return function(key); 
      } 
     } 
     finally 
     {   
      lock (keyLocks) 
      { 
       if (keyLock != null && --keyLock.Count == 0) keyLocks.Remove(key); 
      } 
     } 
    } 

    public void ExecuteSynchronized(TKey key, Action<TKey> action) 
    { 
     this.ExecuteSynchronized<bool>(key, k => 
     { 
      action(k); 
      return true; 
     }); 
    } 
} 

E usato in questo modo:

private locker = new KeyLocker<string>(); 
...... 

void UseLocker() 
{ 
    locker.ExecuteSynchronized("some string",() => DoSomething()); 
} 
Problemi correlati