HOW TO:在 ConcurrentDictionary 中加入和移除項目
這個範例將示範如何在 System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue> 中加入、擷取、更新和移除項目。 這個集合類別是安全執行緒實作。 每當多個執行緒可能會嘗試以並行方式存取項目時,我們建議您使用此集合類別。
ConcurrentDictionary<TKey, TValue> 提供了許多簡便方法,可讓程式碼不需要先檢查機碼是否存在,然後再嘗試加入或移除資料。 下表列出這些簡便方法並描述它們的使用時機。
方法 |
使用時機... |
---|---|
您想要針對指定的機碼加入新的值,而且如果機碼已經存在,您想要取代其值。 |
|
您想要針對指定的機碼擷取現有的值,而且如果機碼不存在,您想要指定機碼值組。 |
|
您想要加入、取得、擷取或移除機碼值組,而且如果機碼已經存在或嘗試由於任何其他原因而失敗,您想要採取某個替代動作。 |
範例
下列範例會使用兩個 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) threadA 呼叫 GetOrAdd,發現沒有任何項目,並且建立要藉由叫用 valueFactory 委派加入的新項目。
2) threadB 同時呼叫 GetOrAdd,其 valueFactory 委派會被叫用並且在 threadA 之前抵達內部鎖定,因此它的新機碼值組會加入字典中。
3) threadA 的使用者委派完成,且該執行緒抵達鎖定,但是現在發現該項目已存在
4) threadA 執行 "Get",並且傳回之前由 threadB 加入的資料。
因此並不保證 GetOrAdd 傳回的資料就是執行緒的 valueFactory 所建立的資料。 呼叫 AddOrUpdate 時,也可能發生類似的事件序列。