Поделиться через


Практическое руководство. Добавление элементов в коллекцию ConcurrentDictionary и их удаление из этой коллекции

В этом примере показано, как выполняется добавление, получение, обновление и удаление элементов из класса System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue>. Этот класс коллекций является потокобезопасной реализацией. Рекомендуется его использование каждый раз, когда множество потоков одновременно могут попытаться получить доступ к элементам.

Класс ConcurrentDictionary<TKey, TValue> предоставляет несколько удобных методов, которые устраняют необходимость создания кода для первоначальной проверки существования ключа перед попыткой добавления или удаления данных. В таблице ниже перечислены эти удобные методы и приводится описание их использования.

Метод

Следует использовать в следующих случаях...

AddOrUpdate

Необходимо добавить новое значение для заданного ключа, а также в том случае если ключ уже существует и необходимо заменить его значение.

GetOrAdd

Необходимо получить существующее значение для заданного ключа, а также в том случае если ключ не существует и задать значение паре "ключ-значение".

TryAdd, TryGetValue , TryUpdate , TryRemove

Необходимо добавить, получить, обновить или удалить пару "ключ-значение", а также в том случае, если ключ уже существует или попытка завершилась по какой-либо причине ошибкой и необходимо выполнить альтернативные действия.

Пример

В следующем примере два экземпляра класса Task используются для одновременного добавления некоторых элементов в класс ConcurrentDictionary<TKey, TValue>, затем выполняется вывод всего содержимого, для того, чтобы показать, что добавление элементов выполнено успешно. В примере также показано, как использовать метод AddOrUpdate, TryGetValue, GetOrAdd и метод TryRemove для добавления, обновления и удаления элементов из коллекции.

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> разработан для многопотоковых сценариев. Необязательно использовать блокировки в коде для добавления или удаления элементов из коллекции. Однако всегда есть возможность для одного потока получить значение, а для другого потока немедленно обновить коллекцию, передавая тому же ключу новое значение.

Кроме того, несмотря на то что все методы ConcurrentDictionary<TKey, TValue> потокобезопасны, не все методы атомарны, например GetOrAdd и AddOrUpdate. Пользовательский делегат, передаваемый этим методам, вызывается вне внутренней блокировки словаря. (Это позволяет предотвратить блокировку всех потоков неизвестным кодом.) Поэтому может произойти следующая последовательность событий.

1) Поток A вызывает GetOrAdd, не находит элемента и создает новый элемент для добавления, вызывая делегат valueFactory.

2) Поток B одновременно вызывает GetOrAdd, делегат valueFactory вызван, этот поток оказывается во внутренней блокировке раньше потока A, его новая пара ключ-значение добавляется в словарь.

3) Пользовательский делегат потока A завершает работу, поток достигает блокировки, но видит, что теперь элемент уже существует.

4) Поток A выполняет действие "Get" и возвращает данные, добавленные ранее потоком B.

Поэтому нет гарантии, что данные, возвращаемые методом GetOrAdd, будут данными, созданными делегатом valueFactory потока. Аналогичная последовательность событий может иметь место при вызове метода AddOrUpdate.

См. также

Ссылки

System.Collections.Concurrent

Основные понятия

Потокобезопасные коллекции