Guide pratique pour ajouter et supprimer des éléments d’un ConcurrentDictionary
Cet exemple montre comment ajouter, extraire, mettre à jour et supprimer des éléments dans un System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue>. Cette classe de collection est une implémentation thread-safe. Nous vous recommandons de l’utiliser chaque fois que plusieurs threads tentent d’accéder aux éléments de façon simultanée.
ConcurrentDictionary<TKey,TValue> fournit plusieurs méthodes pratiques qui permettent au code de ne plus avoir à vérifier au préalable si une clé existe avant d’essayer d’ajouter ou de supprimer des données. Le tableau suivant répertorie ces méthodes pratiques et indique quand les utiliser.
Méthode | À utiliser quand… |
---|---|
AddOrUpdate | Vous voulez ajouter une nouvelle valeur pour une clé spécifiée et, si la clé existe déjà, vous souhaitez remplacer sa valeur. |
GetOrAdd | Vous voulez récupérer la valeur existante d’une clé spécifiée et, si la clé n’existe pas, vous voulez spécifier une paire clé/valeur. |
TryAdd, TryGetValue, TryUpdate, TryRemove | Vous voulez ajouter, obtenir, mettre à jour ou supprimer une paire clé/valeur, et, si la clé existe déjà ou si la tentative échoue pour une autre raison, vous voulez prendre une autre mesure. |
Exemple
L’exemple suivant utilise deux instances deTask pour ajouter des éléments à un ConcurrentDictionary<TKey,TValue> simultanément, puis sort tout le contenu pour montrer que les éléments ont été ajoutés avec succès. L’exemple montre également comment utiliser les méthodes AddOrUpdate, TryGetValue et GetOrAdd pour ajouter, mettre à jour et récupérer des éléments de la collection.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace DictionaryHowTo
{
// The type of the Value to store in the dictionary.
class CityInfo : IEqualityComparer<CityInfo>
{
public string Name { get; set; }
public DateTime LastQueryDate { get; set; } = DateTime.Now;
public decimal Longitude { get; set; } = decimal.MaxValue;
public decimal Latitude { get; set; } = decimal.MaxValue;
public int[] RecentHighTemperatures { get; set; } = new int[] { 0 };
public bool Equals(CityInfo x, CityInfo y)
=> (x.Name, x.Longitude, x.Latitude) ==
(y.Name, y.Longitude, y.Latitude);
public int GetHashCode(CityInfo cityInfo) =>
cityInfo?.Name.GetHashCode() ?? throw new ArgumentNullException(nameof(cityInfo));
}
class Program
{
static readonly ConcurrentDictionary<string, CityInfo> Cities =
new ConcurrentDictionary<string, CityInfo>(StringComparer.OrdinalIgnoreCase);
static async Task Main()
{
CityInfo[] cityData =
{
new CityInfo { Name = "Boston", Latitude = 42.358769m, Longitude = -71.057806m, RecentHighTemperatures = new int[] { 56, 51, 52, 58, 65, 56,53} },
new CityInfo { Name = "Miami", Latitude = 25.780833m, Longitude = -80.195556m, RecentHighTemperatures = new int[] { 86,87,88,87,85,85,86 } },
new CityInfo { Name = "Los Angeles", Latitude = 34.05m, Longitude = -118.25m, RecentHighTemperatures = new int[] { 67,68,69,73,79,78,78 } },
new CityInfo { Name = "Seattle", Latitude = 47.609722m, Longitude = -122.333056m, RecentHighTemperatures = new int[] { 49,50,53,47,52,52,51 } },
new CityInfo { Name = "Toronto", Latitude = 43.716589m, Longitude = -79.340686m, RecentHighTemperatures = new int[] { 53,57, 51,52,56,55,50 } },
new CityInfo { Name = "Mexico City", Latitude = 19.432736m, Longitude = -99.133253m, RecentHighTemperatures = new int[] { 72,68,73,77,76,74,73 } },
new CityInfo { Name = "Rio de Janeiro", Latitude = -22.908333m, Longitude = -43.196389m, RecentHighTemperatures = new int[] { 72,68,73,82,84,78,84 } },
new CityInfo { Name = "Quito", Latitude = -0.25m, Longitude = -78.583333m, RecentHighTemperatures = new int[] { 71,69,70,66,65,64,61 } },
new CityInfo { Name = "Milwaukee", Latitude = -43.04181m, Longitude = -87.90684m, RecentHighTemperatures = new int[] { 32,47,52,64,49,44,56 } }
};
// Add some key/value pairs from multiple threads.
await Task.WhenAll(
Task.Run(() => TryAddCities(cityData)),
Task.Run(() => TryAddCities(cityData)));
static void TryAddCities(CityInfo[] cities)
{
for (var i = 0; i < cities.Length; ++i)
{
var (city, threadId) = (cities[i], Thread.CurrentThread.ManagedThreadId);
if (Cities.TryAdd(city.Name, city))
{
Console.WriteLine($"Thread={threadId}, added {city.Name}.");
}
else
{
Console.WriteLine($"Thread={threadId}, could not add {city.Name}, it was already added.");
}
}
}
// Enumerate collection from the app main thread.
// Note that ConcurrentDictionary is the one concurrent collection
// that does not support thread-safe enumeration.
foreach (var city in Cities)
{
Console.WriteLine($"{city.Key} has been added.");
}
AddOrUpdateWithoutRetrieving();
TryRemoveCity();
RetrieveValueOrAdd();
RetrieveAndUpdateOrAdd();
Console.WriteLine("Press any key.");
Console.ReadKey();
}
// This method shows how to add key-value pairs to the dictionary
// in scenarios where the key might already exist.
static void AddOrUpdateWithoutRetrieving()
{
// Sometime later. We receive new data from some source.
var ci = new CityInfo
{
Name = "Toronto",
Latitude = 43.716589M,
Longitude = -79.340686M,
RecentHighTemperatures = new int[] { 54, 59, 67, 82, 87, 55, -14 }
};
// Try to add data. If it doesn't exist, the object ci is added. If it does
// already exist, update existingVal according to the custom logic.
_ = Cities.AddOrUpdate(
ci.Name,
ci,
(cityName, existingCity) =>
{
// If this delegate is invoked, then the key already exists.
// Here we make sure the city really is the same city we already have.
if (ci != existingCity)
{
// throw new ArgumentException($"Duplicate city names are not allowed: {ci.Name}.");
}
// The only updatable fields are the temperature array and LastQueryDate.
existingCity.LastQueryDate = DateTime.Now;
existingCity.RecentHighTemperatures = ci.RecentHighTemperatures;
return existingCity;
});
// Verify that the dictionary contains the new or updated data.
Console.Write($"Most recent high temperatures for {ci.Name} are: ");
var temps = Cities[ci.Name].RecentHighTemperatures;
Console.WriteLine(string.Join(", ", temps));
}
// This method shows how to use data and ensure that it has been
// added to the dictionary.
static void RetrieveValueOrAdd()
{
var searchKey = "Caracas";
CityInfo retrievedValue = null;
try
{
retrievedValue = Cities.GetOrAdd(searchKey, GetDataForCity(searchKey));
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
}
// Use the data.
if (retrievedValue != null)
{
Console.Write($"Most recent high temperatures for {retrievedValue.Name} are: ");
var temps = Cities[retrievedValue.Name].RecentHighTemperatures;
Console.WriteLine(string.Join(", ", temps));
}
}
// This method shows how to remove a value from the dictionary.
// If the value is unable to be removed, you can handle that by using the return
// boolean value from the .TryRemove function.
static void TryRemoveCity()
{
Console.WriteLine($"Total cities = {Cities.Count}");
var searchKey = "Milwaukee";
if (Cities.TryRemove(searchKey, out CityInfo retrievedValue))
{
Console.Write($"Most recent high temperatures for {retrievedValue.Name} are: ");
var temps = retrievedValue.RecentHighTemperatures;
Console.WriteLine(string.Join(", ", temps));
}
else
{
Console.WriteLine($"Unable to remove {searchKey}");
}
Console.WriteLine($"Total cities = {Cities.Count}");
}
// This method shows how to retrieve a value from the dictionary,
// when you expect that the key/value pair already exists,
// and then possibly update the dictionary with a new value for the key.
static void RetrieveAndUpdateOrAdd()
{
var searchKey = "Buenos Aires";
if (Cities.TryGetValue(searchKey, out CityInfo retrievedValue))
{
// Use the data.
Console.Write($"Most recent high temperatures for {retrievedValue.Name} are: ");
var temps = retrievedValue.RecentHighTemperatures;
Console.WriteLine(string.Join(", ", temps));
// Make a copy of the data. Our object will update its LastQueryDate automatically.
var newValue = new CityInfo
{
Name = retrievedValue.Name,
Latitude = retrievedValue.Latitude,
Longitude = retrievedValue.Longitude,
RecentHighTemperatures = retrievedValue.RecentHighTemperatures
};
// Replace the old value with the new value.
if (!Cities.TryUpdate(searchKey, newValue, retrievedValue))
{
// The data was not updated. Log error, throw exception, etc.
Console.WriteLine($"Could not update {retrievedValue.Name}");
}
}
else
{
// Add the new key and value. Here we call a method to retrieve
// the data. Another option is to add a default value here and
// update with real data later on some other thread.
var newValue = GetDataForCity(searchKey);
if (Cities.TryAdd(searchKey, newValue))
{
// Use the data.
Console.Write($"Most recent high temperatures for {newValue.Name} are: ");
var temps = newValue.RecentHighTemperatures;
Console.WriteLine(string.Join(", ", temps));
}
else
{
Console.WriteLine($"Unable to add data for {searchKey}");
}
}
}
// Assume this method knows how to find long/lat/temp info for any specified city.
static CityInfo GetDataForCity(string name) => name switch
{
"Caracas" => new CityInfo
{
Name = "Caracas",
Longitude = 10.5M,
Latitude = -66.916667M,
RecentHighTemperatures = new int[] { 91, 89, 91, 91, 87, 90, 91 }
},
"Buenos Aires" => new CityInfo
{
Name = "Buenos Aires",
Longitude = -34.61M,
Latitude = -58.369997M,
RecentHighTemperatures = new int[] { 80, 86, 89, 91, 84, 86, 88 }
},
_ => throw new ArgumentException($"Cannot find any data for {name}")
};
}
}
Imports System.Collections.Concurrent
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.Threading
Imports System.Threading.Tasks
Namespace DictionaryHowToVB
' The type of the value to store in the dictionary.
Class CityInfo
Public Property Name As String
Public Property LastQueryDate As DateTime
Public Property Longitude As Decimal
Public Property Latitude As Decimal
Public Property RecentHighTemperatures As Integer()
Public Sub New()
End Sub
Public Sub New(key As String)
Name = key
' MaxValue means "not initialized".
Longitude = Decimal.MaxValue
Latitude = Decimal.MaxValue
LastQueryDate = DateTime.Now
RecentHighTemperatures = {0}
End Sub
Public Sub New(name As String, longitude As Decimal, latitude As Decimal, temps As Integer())
Me.Name = name
Me.Longitude = longitude
Me.Latitude = latitude
RecentHighTemperatures = temps
End Sub
End Class
Class Program
' Create a new concurrent dictionary with the specified concurrency level and capacity.
Shared cities As New ConcurrentDictionary(Of String, CityInfo)(System.Environment.ProcessorCount, 10)
Shared Sub Main()
Dim data As CityInfo() =
{New CityInfo With {.Name = "Boston", .Latitude = 42.358769, .Longitude = -71.057806, .RecentHighTemperatures = {56, 51, 52, 58, 65, 56, 53}},
New CityInfo With {.Name = "Miami", .Latitude = 25.780833, .Longitude = -80.195556, .RecentHighTemperatures = {86, 87, 88, 87, 85, 85, 86}},
New CityInfo With {.Name = "Los Angeles", .Latitude = 34.05, .Longitude = -118.25, .RecentHighTemperatures = {67, 68, 69, 73, 79, 78, 78}},
New CityInfo With {.Name = "Seattle", .Latitude = 47.609722, .Longitude = -122.333056, .RecentHighTemperatures = {49, 50, 53, 47, 52, 52, 51}},
New CityInfo With {.Name = "Toronto", .Latitude = 43.716589, .Longitude = -79.340686, .RecentHighTemperatures = {53, 57, 51, 52, 56, 55, 50}},
New CityInfo With {.Name = "Mexico City", .Latitude = 19.432736, .Longitude = -99.133253, .RecentHighTemperatures = {72, 68, 73, 77, 76, 74, 73}},
New CityInfo With {.Name = "Rio de Janiero", .Latitude = -22.908333, .Longitude = -43.196389, .RecentHighTemperatures = {72, 68, 73, 82, 84, 78, 84}},
New CityInfo With {.Name = "Quito", .Latitude = -0.25, .Longitude = -78.583333, .RecentHighTemperatures = {71, 69, 70, 66, 65, 64, 61}}}
' Add some key/value pairs from multiple threads.
Dim tasks(1) As Task
tasks(0) = Task.Run(Sub()
For i As Integer = 0 To 1
If cities.TryAdd(data(i).Name, data(i)) Then
Console.WriteLine($"Added {data(i).Name} on thread {Thread.CurrentThread.ManagedThreadId}")
Else
Console.WriteLine($"Could not add {data(i)}")
End If
Next
End Sub)
tasks(1) = Task.Run(Sub()
For i As Integer = 2 To data.Length - 1
If cities.TryAdd(data(i).Name, data(i)) Then
Console.WriteLine($"Added {data(i).Name} on thread {Thread.CurrentThread.ManagedThreadId}")
Else
Console.WriteLine($"Could not add {data(i)}")
End If
Next
End Sub)
' Output results so far.
Task.WaitAll(tasks)
' Enumerate data on main thread. Note that
' ConcurrentDictionary is the one collection class
' that does not support thread-safe enumeration.
For Each city In cities
Console.WriteLine($"{city.Key} has been added")
Next
AddOrUpdateWithoutRetrieving()
RetrieveValueOrAdd()
RetrieveAndUpdateOrAdd()
Console.WriteLine("Press any key")
Console.ReadKey()
End Sub
' This method shows how to add key-value pairs to the dictionary
' in scenarios where the key might already exist.
Private Shared Sub AddOrUpdateWithoutRetrieving()
' Sometime later. We receive new data from some source.
Dim ci As New CityInfo With {.Name = "Toronto", .Latitude = 43.716589, .Longitude = -79.340686, .RecentHighTemperatures = {54, 59, 67, 82, 87, 55, -14}}
' Try to add data. If it doesn't exist, the object ci is added. If it does
' already exist, update existingVal according to the custom logic in the
' delegate.
cities.AddOrUpdate(ci.Name, ci, Function(key, existingVal)
' If this delegate is invoked, then the key already exists.
' Here we make sure the city really is the same city we already have.
' (Support for multiple keys of the same name is left as an exercise for the reader.)
If (ci.Name = existingVal.Name And ci.Longitude = existingVal.Longitude) = False Then
Throw New ArgumentException($"Duplicate city names are not allowed: {ci.Name}.")
End If
' The only updatable fields are the temperature array and LastQueryDate.
existingVal.LastQueryDate = DateTime.Now
existingVal.RecentHighTemperatures = ci.RecentHighTemperatures
Return existingVal
End Function)
' Verify that the dictionary contains the new or updated data.
Console.Write($"Most recent high temperatures for {cities(ci.Name).Name} are: ")
Dim temps = cities(ci.Name).RecentHighTemperatures
For Each temp In temps
Console.Write($"{temp}, ")
Next
Console.WriteLine()
End Sub
'This method shows how to use data and ensure that it has been
' added to the dictionary.
Private Shared Sub RetrieveValueOrAdd()
Dim searchKey = "Caracas"
Dim retrievedValue As CityInfo = Nothing
Try
retrievedValue = cities.GetOrAdd(searchKey, GetDataForCity(searchKey))
Catch e As ArgumentException
Console.WriteLine(e.Message)
End Try
' Use the data.
If Not retrievedValue Is Nothing Then
Console.WriteLine($"Most recent high temperatures for {retrievedValue.Name} are: ")
Dim temps = cities(retrievedValue.Name).RecentHighTemperatures
For Each temp In temps
Console.Write($"{temp}, ")
Next
End If
Console.WriteLine()
End Sub
' This method shows how to retrieve a value from the dictionary,
' when you expect that the key/value pair already exists,
' and then possibly update the dictionary with a new value for the key.
Private Shared Sub RetrieveAndUpdateOrAdd()
Dim retrievedValue As New CityInfo()
Dim searchKey = "Buenos Aires"
If (cities.TryGetValue(searchKey, retrievedValue)) Then
' Use the data.
Console.Write($"Most recent high temperatures for {retrievedValue.Name} are: ")
Dim temps = retrievedValue.RecentHighTemperatures
For Each temp In temps
Console.Write($"{temp}, ")
Next
' Make a copy of the data. Our object will update its LastQueryDate automatically.
Dim newValue As New CityInfo(retrievedValue.Name,
retrievedValue.Longitude,
retrievedValue.Latitude,
retrievedValue.RecentHighTemperatures)
Else
Console.WriteLine($"Unable to find data for {searchKey}")
End If
End Sub
' Assume this method knows how to find long/lat/temp info for any specified city.
Private Shared Function GetDataForCity(searchKey As String) As CityInfo
' Real implementation left as exercise for the reader.
If String.CompareOrdinal(searchKey, "Caracas") = 0 Then
Return New CityInfo() With {.Name = "Caracas",
.Longitude = 10.5,
.Latitude = -66.916667,
.RecentHighTemperatures = {91, 89, 91, 91, 87, 90, 91}}
ElseIf String.CompareOrdinal(searchKey, "Buenos Aires") = 0 Then
Return New CityInfo() With {.Name = "Buenos Aires",
.Longitude = -34.61,
.Latitude = -58.369997,
.RecentHighTemperatures = {80, 86, 89, 91, 84, 86, 88}}
Else
Throw New ArgumentException($"Cannot find any data for {searchKey}")
End If
End Function
End Class
End Namespace
ConcurrentDictionary<TKey,TValue> est conçu pour les scénarios multithreads. Vous n’avez pas besoin d’utiliser des verrous dans votre code pour ajouter ou supprimer des éléments dans la collection. Toutefois, il est toujours possible pour un thread de récupérer une valeur et pour un autre thread de mettre à jour immédiatement la collection en donnant une nouvelle valeur à la même clé.
En outre, bien que toutes les méthodes de ConcurrentDictionary<TKey,TValue> soient thread-safe, toutes les méthodes sont atomiques, en particulier GetOrAdd et AddOrUpdate. Pour empêcher du code inconnu de bloquer tous les threads, le délégué utilisateur passé à ces méthodes est appelé en dehors du verrou interne du dictionnaire. Par conséquent, la séquence d’événements suivante peut se produire :
Le threadA appelle GetOrAdd, ne trouve pas d’élément et crée un autre élément à ajouter en appelant le délégué
valueFactory
.Le threadB appelle GetOrAdd simultanément, son délégué
valueFactory
est appelé et il atteint le verrou interne avant le threadA. Sa nouvelle paire clé-valeur est alors ajoutée au dictionnaire.Le délégué utilisateur du threadA termine l’exécution. Le thread atteint le verrou, mais il constate que l’élément existe déjà.
Le threadA exécute une commande "Get" et retourne les données déjà ajoutées par le threadB.
Cela ne garantit donc pas que les données retournées par GetOrAdd sont les mêmes que celles créées par le délégué valueFactory
du thread. Une séquence d’événements similaire peut se produire lors de l’appel de la méthode AddOrUpdate.