2011-11-16 17 views
8

Si sta verificando questa eccezione nel seguente blocco di codice in un contesto ASP.NET in esecuzione su un server IIS 7.Eccezione durante l'aggiunta della voce del dizionario

1) Exception Information 
********************************************* 
Exception Type: System.Exception 
Message: Exception Caught in Application_Error event 
Error in: InitializationStatus.aspx 
Error Message:An item with the same key has already been added. 
Stack Trace: at 
System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add) 
at CredentialsSession.GetXmlSerializer(Type serializerType) 

Questo è il codice che l'eccezione si sta verificando in:

[Serializable()] 
public class CredentialsSession 
{ 
    private static Dictionary<string, System.Xml.Serialization.XmlSerializer> localSerializers = new Dictionary<string, XmlSerializer>(); 

    private System.Xml.Serialization.XmlSerializer GetXmlSerializer(Type serializerType) 
    { 
     string sessionObjectName = serializerType.ToString() + ".Serializer"; 

     if (Monitor.TryEnter(this)) 
     { 
      try 
      { 
       if (!localSerializers.ContainsKey(sessionObjectName)) 
       { 
        localSerializers.Add(sessionObjectName, CreateSerializer(serializerType)); 
       } 
      } 
      finally 
      { 
       Monitor.Exit(this); 
      } 
     } 
     return localSerializers[sessionObjectName]; 
    } 

    private System.Xml.Serialization.XmlSerializer CreateSerializer(Type serializerType) 
    { 
     XmlAttributes xmlAttributes = GetXmlOverrides(); 

     XmlAttributeOverrides xmlOverrides = new XmlAttributeOverrides(); 
     xmlOverrides.Add(typeof(ElementBase), "Elements", xmlAttributes); 

     System.Xml.Serialization.XmlSerializer serializer = 
      new System.Xml.Serialization.XmlSerializer(serializerType, xmlOverrides); 

     return serializer; 
    } 
} 

Il Monitor.TryEnter dovrebbe essere impedire più thread di entrare nel blocco contemporaneamente, e il codice sta controllando il dizionario di verificare che non contenga la chiave che viene aggiunta.

Qualche idea su come questo potrebbe accadere?

+0

+1, per la domanda che spiega come trovare un duplicato nel Dizionario. –

risposta

5

Il tuo codice non è thread-safe.

  1. Stai blocco su this, un'istanza CredentialsSession, ma l'accesso a un dizionario statica che può essere condiviso da più CredentialsSession istanze. Questo spiega perché stai ricevendo l'errore: due diverse istanze di CredentialsSession stanno tentando di scrivere contemporaneamente nel dizionario.

  2. Anche se lo si modifica per bloccare un campo statico come suggerito nella risposta di @ sll, non si è sicuri del thread, perché non si sta bloccando durante la lettura del dizionario. È necessario un ReaderWriterLock o ReaderWriterLockSlim per consentire in modo efficiente più lettori e un singolo writer.

    Pertanto è consigliabile utilizzare un dizionario thread-safe. ConcurrentDictionary come altri hanno detto se stai usando .NET 4.0. In caso contrario, è necessario implementare il proprio o utilizzare un'implementazione esistente come http://devplanet.com/blogs/brianr/archive/2008/09/26/thread-safe-dictionary-in-net.aspx.

I commenti suggeriscono di voler evitare di chiamare lo stesso tipo più volte per lo stesso tipo. Non so perché, perché il vantaggio prestazionale è probabilmente trascurabile, dal momento che la contesa è probabile che sia rara e non può superare una volta per ogni tipo durante la vita dell'applicazione.

Ma se si vuole veramente questo, si può fare come segue:

var value; 
if (!dictionary.TryGetValue(key, out value)) 
{ 
    lock(dictionary) 
    { 
     if(!dictionary.TryGetValue(key, out value)) 
     { 
      value = CreateSerializer(...); 
      dictionary[key] = value; 
     } 
    } 
} 

Da commento:

se a implementare questo con ConcurrentDictionary e semplicemente chiamo TryAdd (sessionObjectName, CreateSerializer (serializerType)) ogni volta.

La risposta è di non chiamare TryAggiungi ogni volta - prima controlla se è nel dizionario, quindi aggiungi se non lo è. Un'alternativa migliore potrebbe essere l'utilizzo di the GetOrAdd overload che accetta un argomento Func.

+0

Non avevo notato che il dizionario è statico! Questo spiega molto, grazie! Sto ri-implementando questo con ConcurrentDictionary. – Avalanchis

+0

Supponendo che GetXmlSerializer venga chiamato frequentemente per recuperare i valori dal Dizionario, se si implementa questo con ConcurrentDictionary e si chiama semplicemente TryAdd (sessionObjectName, CreateSerializer (serializerType)) ogni volta, quindi CreateSerializer verrà chiamato ogni volta che viene chiamato GetXmlSerializer. Sembra che vorrei evitare questo. Una volta che il dizionario è popolato per ciascun valore chiave, non c'è più motivo di chiamare CreateSerializer.Corretta? – Avalanchis

+2

+1 poiché questa risposta descrive completamente il problema esistente e suggerisce come risolverlo per diversi casi – sll

5

Provare a bloccare su localSerializers piuttosto che this. A proposito, perché stai usando Monitor esplicitamente? Solo una ragione che vedo è quello di fornire lock timeout che ovviamente non lo si utilizza, in modo da utilizzare semplicemente lock() statement invece si realizzerebbe try/finally così:

lock (localSerializers) 
{ 
    if (!localSerializers.ContainsKey(sessionObjectName))     
    {      
     localSerializers.Add(
      sessionObjectName, 
      CreateSerializer(serializerType));     
    } 
} 

EDIT: Dal momento che non è stato specificato nei tag che si sta utilizzando .NET 4 io suggerirei di usare ConcurrentDictionary<TKey, TValue>


Monitor.Enter() Method:

Utilizzare un C# try ... finally block (Try ... Finally in Visual Basic) per garantire che si rilascia il monitor o utilizzare l'istruzione C# lock (SyncLock in Visual Basic), che include i metodi Enter e Exit in un blocco try ... finally

+0

Grazie per la risposta! Non originariamente il mio codice, quindi non posso fornire una risposta per il motivo per cui sta usando Monitor esplicitamente. Ha perfettamente senso bloccare la risorsa che viene protetta piuttosto che questa. Ci proverò, grazie! – Avalanchis

+0

@Avalanchis: vedi EDIT parte della risposta aggiornata, inoltre ho aggiunto .NET 4 tag per la tua domanda, è piuttosto importante – sll

+0

Mi piace l'idea di usare ConcurrentDictionary, ma sono preoccupato per le chiamate non necessarie a CreateSerializer. Sembra che debba ancora chiamare ContainsKey per vedere se la chiave esiste prima di chiamare TryAdd se voglio evitare di chiamare CreateSerializer ogni volta. Corretta? – Avalanchis

1

Se siete su .NET Framework 4 o versione successiva, vorrei suggerire di utilizzare un ConcurrentDictionary invece. Il metodo TryAdd ti mantiene al sicuro da questo tipo di scenario, senza la necessità di lettiera il codice con le serrature:

localSerializers.TryAdd(sessionObjectName, CreateSerializer(serializerType)) 

Se siete preoccupati per CreateSerializer ad essere invocate quando non è necessario, si dovrebbe invece usare AddOrUpdate:

localSerializers.AddOrUpdate(
    sessionObjectName, 
    key => CreateSerialzer(serializerType), 
    (key, value) => value); 

Ciò garantisce che il metodo venga chiamato solo quando è necessario produrre un nuovo valore (quando è necessario aggiungerlo al dizionario). Se è già presente, la voce verrà "aggiornata" con il valore già esistente.

+0

Stiamo usando .NET 4. Avrebbe ancora senso verificare se ConcurrentDictionary contiene la chiave da aggiungere prima? Sono preoccupato che TryAdd possa chiamare inutilmente CreateSerializer se la chiave esiste già. – Avalanchis

+0

@Avalanchis: vedere la risposta aggiornata. –

Problemi correlati