ConcurrentDictionary Pitfall – Le fabbriche dei delegati da GetOrAdd e AddOrUpdate sono sincronizzate?

La documentazione di ConcurrentDictionary non è uno stato esplicito, quindi suppongo che non ci si possa aspettare che i delegati valueFactory e updateValueFactory abbiano la loro esecuzione sincronizzata (rispettivamente dalle operazioni GetOrAdd () e AddOrUpdate () ).

Quindi, penso che non possiamo implementare l’uso di risorse al loro interno che richiedono un controllo concorrente senza implementare manualmente il nostro controllo concorrente, magari usando semplicemente [MethodImpl(MethodImplOptions.Synchronized)] sui delegati.

Ho ragione? O il fatto che ConcurrentDictionary sia thread-safe possiamo aspettarci che le chiamate a questi delegati siano automaticamente sincronizzate (thread-safe)?

Sì, hai ragione, i delegati utente non sono sincronizzati da ConcurrentDictionary . Se hai bisogno di quelli sincronizzati è una tua responsabilità.

Lo stesso MSDN dice:

Inoltre, sebbene tutti i metodi di ConcurrentDictionary siano thread-safe, non tutti i metodi sono atomici, in particolare GetOrAdd e AddOrUpdate. L’utente delegato passato a questi metodi viene richiamato al di fuori del blocco interno del dizionario. (Questo è fatto per impedire al codice sconosciuto di bloccare tutti i thread.)

Vedere “Procedura: aggiungere e rimuovere elementi da un concurrentDictionary

Questo perché il ConcurrentDictionary non ha idea di cosa farà il delegato che fornisci o delle sue prestazioni, quindi se tentasse di bloccarle, potrebbe davvero avere un impatto negativo sulle prestazioni e rovinare il valore di ConcurrentDictionary.

Pertanto, è responsabilità dell’utente sincronizzare i propri delegati se ciò è necessario. Il link MSDN sopra in realtà ha un buon esempio delle garanzie che fa e non produce.

Questi delegati non solo non sono sincronizzati, ma non è nemmeno garantito che si verifichino una sola volta. Possono, infatti, essere eseguiti più volte per chiamata a AddOrUpdate .

Ad esempio, l’algoritmo per AddOrUpdate è simile a questo.

 TValue value; do { if (!TryGetValue(...)) { value = addValueFactory(key); if (!TryAddInternal(...)) { continue; } return value; } value = updateValueFactory(key); } while (!TryUpdate(...)) return value; 

Nota due cose qui.

  • Non c’è alcuno sforzo per sincronizzare l’esecuzione dei delegati.
  • I delegati possono essere eseguiti più di una volta poiché vengono richiamati all’interno del ciclo.

Quindi devi assicurarti di fare due cose.

  • Fornire la propria sincronizzazione per i delegati.
  • Assicurati che i tuoi delegati non abbiano effetti collaterali che dipendono dal numero di volte in cui vengono eseguiti.