Už před více jak rokem jsem psal do služby Microsoft Connect požadavek, aby byl přidán dodatek do dokumentace k třídě slovníku optimalizovaného do více vláknového prostředí - ConcurrentDictionary. Bylo potřeba upřesnit, jakým způsobem se chovají některé z jeho metod právě při volání z více vláken simultálně. Před nějakou dobou jsem se konečně dočkal a dále popisované chování bylo upřesněno i v dokumentaci. Jen mi není jasné proč jen v dokumentaci .NET 4.5, když chování jsem reportovat už pro .NET 4, kde stále chybí.
Jedná se o metody:
Tyto metody slouží k operacím pro čtení a úpravu položek z tohoto slovníku. Obě metody přijímají delegáta, který se odkazuje na funkci, která se zavolá ve chvíli, kdy slovník zatím neobsahuje žádnou hodnotu s daným klíčem.
Podívejte se na zkrácenou (nekamenujte mě za nedostatky) ukázku kódu, který používá ConcurrentDictionary k uchování pojmenovaných připojení do databáze. Využil jsem metody GetOrAdd pro získání existujícího připojení a zároveň v případě, že připojení neexistuje, vytváří nové metodou CreateNamedConnection. Všechny připojení následně zruším pomocí Dispose metody.
Když jsem tyto metody viděl poprvé, čekal jsem, že se delegát pro vytvoření nové hodnoty zavolá pouze jednou. Ve skutečnost se však může zavolat vícekrát, ale jeho výsledek se použije pouze z jednoho volání. To právě popisuje nově přidaný dodatek z MSDN:
If you call GetOrAdd/AddOrUpdate simultaneously on different threads, addValueFactory may be called multiple times, but its key/value pair might not be added to the dictionary for every call.
Toto může být nepříjemný problém, pokud volání delegáta trvá delší dobu a buď zatěžuje systémové prostředky / přistupuje k not-thread-safe zdrojům a nebo vytváří unmanaged zdroje.
Vše totiž nasvědčuje tomu, že pokud jiné vlákno zavolá znovu jednu z těchto metod v průběhu zpracování kódu delegáta jiného vlákna, ConcurrentDictionary i na tomto novém vlákno delegáta zavolá, ačkoliv nakonec hodnotu, kterou vrátí, nepoužije. Je to protože předchozí vlákno stihne doběhnout a do slovníku se hodnota mezitím přidá pouze jednou.
To demonstruje právě následující kód, který ve většině případů vytvoří vyvoláním delegáta více instancí připojení do databáze, reálně se ale použije jen jedna a v paměti zůstane po nějakou dobu otevřené další nepoužívané a zbytečné připojení. V praxi to tedy znamená, že není vhodné použít tyto metody pro řízení vytváření jen jedné instance pro daný klíč.
static void Main(string[] args)
{
var dictionary = new ConcurrentDictionary<string, SqlConnection>();
// create threads
Thread[] threads = new Thread[20];
for (int i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(() => { ThreadStartMethod(dictionary); });
threads[i].Start();
}
// wait for all threads
foreach (var thread in threads)
{
thread.Join();
}
// dispose all items in dictionary
foreach (var item in dictionary)
{
item.Value.Dispose();
Console.WriteLine("Connection disposed for key \"{0}\".", item.Key);
}
Console.ReadLine();
}
static void ThreadStartMethod(ConcurrentDictionary<string, SqlConnection> dictionary)
{
SqlConnection connection = dictionary.GetOrAdd("ConnectionName", CreateNamedConnection);
// do something with connection
}
private static SqlConnection CreateNamedConnection(string key)
{
Console.WriteLine("New connection opened for key \"{0}\".", key);
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1)); // simulate opening connection
return new SqlConnection("Server=.");
}