Hinzufügen und Entfernen von Elementen aus einem ConcurrentDictionary

Dieses Beispiel zeigt, wie Elemente hinzugefügt, abgerufen, aktualisiert und von einer System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue> entfernt werden. Diese Auflistungsklasse ist eine threadsichere Implementierung. Es empfiehlt sich, diese Klasse immer dann zu verwenden, wenn möglicherweise mehrere Threads gleichzeitig versuchen, auf Elemente zuzugreifen.

ConcurrentDictionary<TKey,TValue> bietet verschiedene praktische Methoden, mit denen der Code nicht mehr zuerst prüfen muss, ob ein Schlüssel vorhanden ist, bevor versucht wird, Daten hinzuzufügen oder zu entfernen. Die folgende Tabelle führt diese praktischen Methoden auf und beschreibt ihre Verwendung.

Methode Wenn folgende Bedingungen vorliegen
AddOrUpdate Sie möchten einen neuen Wert für einen angegebenen Schlüssel hinzufügen und, wenn der Schlüssel bereits vorhanden ist, den Wert ersetzen.
GetOrAdd Sie möchten den vorhandenen Wert für einen angegebenen Schlüssel abrufen und, wenn der Schlüssel nicht vorhanden ist, ein Schlüssel-Wert-Paar angeben.
TryAdd, TryGetValue, TryUpdate, TryRemove Sie möchten ein Schlüssel-Wert-Paar hinzufügen, abrufen, aktualisieren oder entfernen und, wenn der Schlüssel bereits vorhanden ist oder der Versuch aus einem anderen Grund fehlschlägt, eine alternative Aktion durchführen.

Beispiel

Im folgenden Beispiel werden zwei Instanzen von Task verwendet, um ConcurrentDictionary<TKey,TValue> parallel einige Elemente hinzuzufügen und dann den gesamten Inhalt auszugeben, um zu zeigen, dass die Elemente erfolgreich hinzugefügt wurden. Das Beispiel zeigt auch, wie die Methoden AddOrUpdate, TryGetValue und GetOrAdd zum Hinzufügen, Aktualisieren und Abrufen von Elementen aus der Auflistung verwendet werden.

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> wurde für Multithread-Szenarios entworfen. Sie müssen keine Sperren in Ihrem Code verwenden, um Elemente zur Auflistung hinzuzufügen oder daraus zu entfernen. Es ist jedoch immer möglich, dass ein Thread einen Wert abruft und ein anderer Thread durch Zuweisen eines neuen Werts zum gleichen Schlüssel die Auflistung unmittelbar danach aktualisiert.

Darüber hinaus sind zwar alle Methoden von ConcurrentDictionary<TKey,TValue> threadsicher, aber nicht alle Methoden sind atomisch – insbesondere GetOrAdd und AddOrUpdate. Der an diese Methoden übergebene Benutzerdelegat wird außerhalb der internen Sperre des Wörterbuchs aufgerufen, um unbekannten Code davon abzuhalten, alle Threads zu blockieren. Daher ist es möglich, dass die folgende Ereignissequenz eintritt:

  1. threadA ruft GetOrAdd auf, findet kein Element und erstellt ein neues hinzuzufügendes Element durch Aufrufen des valueFactory-Delegaten.

  2. threadB ruft GetOrAdd gleichzeitig auf, der zugehörige valueFactory-Delegat wird aufgerufen und erreicht die interne Sperre vor threadA. Daher wird das neue Schlüssel-Wert-Paar dieses Threads zum Wörterbuch hinzugefügt.

  3. Der Benutzerdelegat von threadA wird fertiggestellt, und der Thread erreicht die Sperre, stellt jetzt aber fest, dass das Element bereits vorhanden ist.

  4. threadA führt eine Get-Aktion aus und gibt die Daten zurück, die zuvor von threadB hinzugefügt wurden.

Daher ist nicht garantiert, dass die von GetOrAdd zurückgegebenen Daten die gleichen Daten sind, die von valueFactory des Threads erstellt wurden. Eine ähnliche Abfolge von Ereignissen kann eintreten, wenn AddOrUpdate aufgerufen wird.

Siehe auch