Gewusst wie: Hinzufügen und Entfernen von Elementen aus einem ConcurrentDictionary
In diesem Beispiel wird gezeigt, wie Elemente einem System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue>-Objekt hinzugefügt werden bzw. wie sie daraus abgerufen, darin aktualisiert und daraus entfernt werden. Diese Auflistungsklasse ist eine threadsichere Implementierung. Es wird empfohlen, die Klasse immer zu verwenden, wenn mehrere Threads möglicherweise gleichzeitig versuchen, auf die Elemente zuzugreifen.
ConcurrentDictionary<TKey, TValue> stellt mehrere Hilfsmethoden bereit, durch die es unnötig wird, zuerst mit dem Code zu überprüfen, ob ein Schlüssel vorhanden ist, bevor versucht wird, Daten hinzuzufügen oder zu entfernen. In der folgenden Tabelle werden diese Hilfsmethoden aufgeführt, und es wird beschrieben, wann sie verwendet werden sollen.
Methode |
Verwendung: |
---|---|
Sie möchten einen neuen Wert für einen angegebenen Schlüssel hinzufügen und bei Vorhandensein des Schlüssels den Wert ersetzen. |
|
Sie möchten den vorhandenen Wert für einen angegebenen Schlüssel abrufen und bei Nichtvorhandensein des Schlüssels möchten Sie ein Schlüssel-Wert-Paar angeben. |
|
Sie möchten ein Schlüssel-Wert-Paar hinzufügen, abrufen, aktualisieren oder entfernen, und bei Vorhandensein des Schlüssels oder bei Fehlschlagen des Versuchs aus einem anderen Grund alternative Aktionen ausführen. |
Beispiel
Im folgenden Beispiel werden einem ConcurrentDictionary<TKey, TValue>-Objekt mithilfe von zwei Task-Instanzen gleichzeitig einige Elemente hinzugefügt. Anschließend wird der gesamte Inhalt ausgegeben, um anzuzeigen, dass die Elemente hinzugefügt wurden. Das Beispiel zeigt überdies, wie mit der AddOrUpdate-Methode, der TryGetValue-Methode, der GetOrAdd-Methode und der TryRemove-Methode der Auflistung Elemente hinzugefügt, darin aktualisiert und daraus abgerufen und entfernt verwendet werden.
Imports System
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
Private _name As String
Property Name As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property
Private _lastQueryDate As DateTime
Property LastQueryDate As DateTime
Get
Return _lastQueryDate
End Get
Set(ByVal value As DateTime)
_lastQueryDate = value
End Set
End Property
Private _longitude As Decimal
Property Longitude As Decimal
Get
Return _longitude
End Get
Set(ByVal value As Decimal)
_longitude = value
End Set
End Property
Private _latitude As Decimal
Property Latitude As Decimal
Get
Return _latitude
End Get
Set(ByVal value As Decimal)
_latitude = value
End Set
End Property
Private _highTemps() As Integer
Property RecentHighTemperatures As Integer()
Get
Return _highTemps
End Get
Set(ByVal value As Integer())
_highTemps = value
End Set
End Property
Public Sub New()
End Sub
Public Sub New(ByVal key As String)
_name = key
' MaxValue means "not initialized"
_longitude = Decimal.MaxValue
_latitude = Decimal.MaxValue
_lastQueryDate = DateTime.Now
_highTemps = {0}
End Sub
Public Sub New(ByVal name As String, ByVal longitude As Decimal,
ByVal latitude As Decimal, ByVal temps As Integer())
_name = name
_longitude = longitude
_latitude = latitude
_highTemps = 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.Factory.StartNew(Sub()
For i As Integer = 0 To 1
If cities.TryAdd(data(i).Name, data(i)) Then
Console.WriteLine("Added {0} on thread {1}", data(i).Name, Thread.CurrentThread.ManagedThreadId)
Else
Console.WriteLine("Could not add {0}", data(i))
End If
Next
End Sub)
tasks(1) = Task.Factory.StartNew(Sub()
For i As Integer = 2 To data.Length - 1
If cities.TryAdd(data(i).Name, data(i)) Then
Console.WriteLine("Added {0} on thread {1}", data(i).Name, Thread.CurrentThread.ManagedThreadId)
Else
Console.WriteLine("Could not add {0}", 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("{0} has been added", city.Key)
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 = 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: {0}.", ci.Name)
End If
' The only updatable fields are the temerature 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 {0} are: ", cities(ci.Name).Name)
Dim temps = cities(ci.Name).RecentHighTemperatures
For Each temp In temps
Console.Write("{0}, ", 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 {0} are: ", retrievedValue.Name)
Dim temps = cities(retrievedValue.Name).RecentHighTemperatures
For Each temp In temps
Console.Write("{0}, ", 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 CityInfo = New CityInfo()
Dim searchKey = "Buenos Aires"
If (cities.TryGetValue(searchKey, retrievedValue)) Then
' Use the data
Console.Write("Most recent high temperatures for {0} are: ", retrievedValue.Name)
Dim temps = retrievedValue.RecentHighTemperatures
For Each temp In temps
Console.Write("{0}, ", temp)
Next
' Make a copy of the data. Our object will update its lastQueryDate automatically.
Dim newValue As CityInfo = New CityInfo(retrievedValue.Name,
retrievedValue.Longitude,
retrievedValue.Latitude,
retrievedValue.RecentHighTemperatures)
Else
Console.WriteLine("Unable to find data for {0}", searchKey)
End If
End Sub
' Assume this method knows how to find long/lat/temp info for any specified city.
Private Shared Function GetDataForCity(ByVal 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 {0}", searchKey)
End If
End Function
End Class
End Namespace
namespace DictionaryHowTo
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
// The type of the Value to store in the dictionary:
class CityInfo : IEqualityComparer<CityInfo>
{
public string Name { get; set; }
public DateTime lastQueryDate { get; set; }
public decimal Longitude { get; set; }
public decimal Latitude { get; set; }
public int[] RecentHighTemperatures { get; set; }
public CityInfo(string name, decimal longitude, decimal latitude, int[] temps)
{
Name = name;
lastQueryDate = DateTime.Now;
Longitude = longitude;
Latitude = latitude;
RecentHighTemperatures = temps;
}
public CityInfo()
{
}
public CityInfo(string key)
{
Name = key;
// MaxValue means "not initialized"
Longitude = Decimal.MaxValue;
Latitude = Decimal.MaxValue;
lastQueryDate = DateTime.Now;
RecentHighTemperatures = new int[] { 0 };
}
public bool Equals(CityInfo x, CityInfo y)
{
return x.Name == y.Name && x.Longitude == y.Longitude && x.Latitude == y.Latitude;
}
public int GetHashCode(CityInfo obj)
{
CityInfo ci = (CityInfo)obj;
return ci.Name.GetHashCode();
}
}
class Program
{
// Create a new concurrent dictionary.
static ConcurrentDictionary<string, CityInfo> cities = new ConcurrentDictionary<string, CityInfo>();
static void Main(string[] args)
{
CityInfo[] data =
{
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 Janiero", 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}}
};
// Add some key/value pairs from multiple threads.
Task[] tasks = new Task[2];
tasks[0] = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 2; i++)
{
if (cities.TryAdd(data[i].Name, data[i]))
Console.WriteLine("Added {0} on thread {1}", data[i],
Thread.CurrentThread.ManagedThreadId);
else
Console.WriteLine("Could not add {0}", data[i]);
}
});
tasks[1] = Task.Factory.StartNew(() =>
{
for (int i = 2; i < data.Length; i++)
{
if (cities.TryAdd(data[i].Name, data[i]))
Console.WriteLine("Added {0} on thread {1}", data[i],
Thread.CurrentThread.ManagedThreadId);
else
Console.WriteLine("Could not add {0}", data[i]);
}
});
// Output results so far.
Task.WaitAll(tasks);
// 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("{0} has been added.", city.Key);
}
AddOrUpdateWithoutRetrieving();
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.
private static void AddOrUpdateWithoutRetrieving()
{
// Sometime later. We receive new data from some source.
CityInfo 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 in the
// delegate.
cities.AddOrUpdate(ci.Name, ci,
(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 cities of the same name is left as an exercise for the reader.)
if (ci != existingVal)
throw new ArgumentException("Duplicate city names are not allowed: {0}.", ci.Name);
// The only updatable fields are the temerature array and lastQueryDate.
existingVal.lastQueryDate = DateTime.Now;
existingVal.RecentHighTemperatures = ci.RecentHighTemperatures;
return existingVal;
});
// Verify that the dictionary contains the new or updated data.
Console.Write("Most recent high temperatures for {0} are: ", cities[ci.Name].Name);
int[] temps = cities[ci.Name].RecentHighTemperatures;
foreach (var temp in temps) Console.Write("{0}, ", temp);
Console.WriteLine();
}
// This method shows how to use data and ensure that it has been
// added to the dictionary.
private static void RetrieveValueOrAdd()
{
string 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 {0} are: ", retrievedValue.Name);
int[] temps = cities[retrievedValue.Name].RecentHighTemperatures;
foreach (var temp in temps) Console.Write("{0}, ", temp);
}
Console.WriteLine();
}
// 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 static void RetrieveAndUpdateOrAdd()
{
CityInfo retrievedValue;
string searchKey = "Buenos Aires";
if (cities.TryGetValue(searchKey, out retrievedValue))
{
// use the data
Console.Write("Most recent high temperatures for {0} are: ", retrievedValue.Name);
int[] temps = retrievedValue.RecentHighTemperatures;
foreach (var temp in temps) Console.Write("{0}, ", temp);
// Make a copy of the data. Our object will update its lastQueryDate automatically.
CityInfo newValue = new CityInfo(retrievedValue.Name,
retrievedValue.Longitude,
retrievedValue.Latitude,
retrievedValue.RecentHighTemperatures);
// Replace the old value with the new value.
if (!cities.TryUpdate(searchKey, retrievedValue, newValue))
{
//The data was not updated. Log error, throw exception, etc.
Console.WriteLine("Could not update {0}", 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.
CityInfo newValue = GetDataForCity(searchKey);
if( cities.TryAdd(searchKey, newValue))
{
// use the data
Console.Write("Most recent high temperatures for {0} are: ", newValue.Name);
int[] temps = newValue.RecentHighTemperatures;
foreach (var temp in temps) Console.Write("{0}, ", temp);
}
else
Console.WriteLine("Unable to add data for {0}", searchKey);
}
}
//Assume this method knows how to find long/lat/temp info for any specified city.
static CityInfo GetDataForCity(string name)
{
// Real implementation left as exercise for the reader.
if (String.CompareOrdinal(name, "Caracas") == 0)
return new CityInfo() { Name = "Caracas",
Longitude = 10.5M,
Latitude = -66.916667M,
RecentHighTemperatures = new int[] { 91, 89, 91, 91, 87, 90, 91 } };
else if (String.CompareOrdinal(name, "Buenos Aires") == 0)
return new CityInfo() { Name = "Buenos Aires",
Longitude = -34.61M,
Latitude = -58.369997M,
RecentHighTemperatures = new int[] { 80, 86, 89, 91, 84, 86, 88 } };
else
throw new ArgumentException("Cannot find any data for {0}", name);
}
}
}
ConcurrentDictionary<TKey, TValue> ist für Multithreadszenarien vorgesehen. Sie müssen keine Sperren im Code verwenden, um der Auflistung Elemente hinzuzufügen oder daraus zu entfernen. Es ist immer jedoch möglich, dass ein Thread einen Wert abruft und dass ein anderer Thread umgehend die Auflistung aktualisiert, indem dem gleichen Schlüssel ein neuer Wert zugewiesen wird.
Obwohl darüber hinaus alle Methoden von ConcurrentDictionary<TKey, TValue> threadsicher sind, sind nicht alle Methoden unteilbar, insbesondere GetOrAdd und AddOrUpdate. Der an diese Methoden übergebene Benutzerdelegat wird außerhalb der internen Sperre des Wörterbuchs aufgerufen. (Hierdurch soll verhindert werden, dass unbekannter Code alle Threads blockiert.) Aus diesem Grund ist es möglich, dass diese Abfolge von Ereignissen eintritt:
1) ThreadA ruft GetOrAdd auf, findet kein Element und erstellt ein neues hinzuzufügendes Element, indem der valueFactory-Delegat aufgerufen wird.
2) ThreadB ruft gleichzeitig GetOrAdd auf, der zugehörige valueFactory-Delegat wird aufgerufen, und die interne Sperre wird vor ThreadA erreicht. Daher wird dem Wörterbuch das neue Schlüssel-Wert-Paar hinzugefügt.
3) Der Benutzerdelegat von ThreadA wird abgeschlossen, und der Thread erreicht die Sperre. Jedoch scheint das neue Element bereits vorhanden zu sein.
4) ThreadA führt "Get" aus und gibt die Daten zurück, die zuvor durch ThreadB hinzugefügt wurden.
Daher ist nicht sichergestellt, dass die von GetOrAdd zurückgegebenen Daten mit den Daten identisch sind, die von valueFactory des Threads erstellt wurden. Eine ähnliche Abfolge von Ereignissen kann eintreten, wenn AddOrUpdate aufgerufen wird.