ConcurrentDictionary.txt. ConcurrentDictionary https://docs.microsoft.com/zh-tw/dotnet/standard/collections/thread-safe/how-to-add-and-remove-items 摘要: 1. 針對多執行緒案例所設計。 不需要在程式碼中使用鎖定,即可新增或移除集合中的項目。 不過,其中一個執行緒一定要可以擷取一個值,而另一個執行緒要將新的值提供給相同的索引鍵來立即更新集合。 這個範例示範如何新增、擷取、更新和移除 System.Collections.Concurrent.ConcurrentDictionary 中的項目。 這個集合類別是安全執行緒實作。 只要多個執行緒可能嘗試同時存取元素,就建議您使用它。 ConcurrentDictionary供幾種便利的方法,讓程式碼不需要在嘗試新增或移除資料之前,先檢查索引鍵存在與否。 下表列出這些便利方法,並描述其使用時機。 方法 使用時機... AddOrUpdate 您想要新增所指定索引鍵的新值,如果已經有索引鍵,則您會想要取代它的值。 GetOrAdd 您想要擷取所指定索引鍵的現有值,如果索引鍵不存在,則您會想要指定索引鍵/值組。 TryAdd, TryGetValue , TryUpdate , TryRemove 您想要新增、取得、更新或移除索引鍵/值組,如果已經有索引鍵,或嘗試因任何其他原因而失敗,則您會想要採取一些替代動作。 下列範例使用兩個 Task 執行個體同時將一些項目新增至 ConcurrentDictionary,然後輸出所有內容以顯示已成功新增項目。 此範例也會示範如何使用 AddOrUpdate、TryGetValue 和 GetOrAdd 方法,在集合中加入、更新和擷取項目。 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 { 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 cities = new ConcurrentDictionary(); 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. var tasks = new Task[2]; tasks[0] = Task.Run(() => { for (int i = 0; i < 2; i++) { if (cities.TryAdd(data[i].Name, data[i])) Console.WriteLine($"Added {data[i]} on thread {Thread.CurrentThread.ManagedThreadId}"); else Console.WriteLine($"Could not add {data[i]}"); } }); tasks[1] = Task.Run(() => { for (int i = 2; i < data.Length; i++) { if (cities.TryAdd(data[i].Name, data[i])) Console.WriteLine($"Added {data[i]} on thread {Thread.CurrentThread.ManagedThreadId}"); else Console.WriteLine($"Could not add {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($"{city.Key} has been added."); } 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. 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 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: {ci.Name}."); // The only updatable fields are the temperature 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 {cities[ci.Name].Name} are: "); int[] temps = cities[ci.Name].RecentHighTemperatures; foreach (var temp in temps) Console.Write($"{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 {retrievedValue.Name} are: "); int[] temps = cities[retrievedValue.Name].RecentHighTemperatures; foreach (var temp in temps) Console.Write($"{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 {retrievedValue.Name} are: "); int[] temps = retrievedValue.RecentHighTemperatures; foreach (var temp in temps) Console.Write($"{temp}, "); // Make a copy of the data. Our object will update its LastQueryDate automatically. var newValue = new CityInfo(retrievedValue.Name, retrievedValue.Longitude, retrievedValue.Latitude, 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. CityInfo newValue = GetDataForCity(searchKey); if (cities.TryAdd(searchKey, newValue)) { // Use the data. Console.Write($"Most recent high temperatures for {newValue.Name} are: "); int[] temps = newValue.RecentHighTemperatures; foreach (var temp in temps) Console.Write($"{temp}, "); } 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) { // 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 {name}"); } } } ConcurrentDictionary 是針對多執行緒案例所設計。 您不需要在程式碼中使用鎖定,即可新增或移除集合中的項目。 不過,其中一個執行緒一定要可以擷取一個值,而另一個執行緒要將新的值提供給相同的索引鍵來立即更新集合。 此外,雖然 ConcurrentDictionary 的所有方法都是安全執行緒,但是並非所有方法都是不可部分完成,尤其是 GetOrAdd 和 AddOrUpdate。 傳遞給這些方法的使用者委派是在字典內部鎖定的外部進行叫用 (完成這項作業是要防止未知程式碼封鎖所有執行緒)。 因此,可能會發生下列序列事件: 1) threadA 會呼叫 GetOrAdd、找不到項目,並叫用 valueFactory 委派來建立要新增的新項目。 2) threadB 同時呼叫 GetOrAdd、叫用其 valueFactory 委派,並且在 threadA 之前到達內部鎖定,因此會將其新的索引鍵/值組新增至字典。 3) threadA 的使用者委派完成,而且執行緒到達鎖定,但是現在會看到這個項目已經存在。 4) threadA 執行 "Get",並傳回 threadB 先前所新增的資料。 因此,不保證 GetOrAdd 所傳回的資料就是執行緒 valueFactory 所建立的相同資料。 呼叫 AddOrUpdate 時,可能會發生一系列類似的事件。 ----------- 20191106 ConcurrentDictionary Represents a thread-safe collection of key/value pairs that can be accessed by multiple threads concurrently. 參考 Concurrent.txt ---------- 20191106 private static void RunYieldTest() { var products = GetStrings(); Dictionary newProducts = new Dictionary(); Console.WriteLine("來源資料筆數:{0}", products.Count()); Console.WriteLine("執行前結果資料筆數:{0}", newProducts.Count()); Parallel.ForEach(products, x => { newProducts.Add(x.Key, x.Value); }); Console.WriteLine("執行後結果資料筆數:{0}", newProducts.Count()); } private static void RunYieldTestConcurrent() { var products = GetStrings(); ConcurrentDictionary newProducts = new ConcurrentDictionary(); Console.WriteLine("來源資料筆數:{0}", products.Count()); Console.WriteLine("執行前結果資料筆數:{0}", newProducts.Count()); Parallel.ForEach(products, x => { newProducts.TryAdd(x.Key, x.Value); }); Console.WriteLine("執行後結果資料筆數:{0}", newProducts.Count()); } ---------- 20191106 class CD_Ctor { // Demonstrates: // ConcurrentDictionary ctor(concurrencyLevel, initialCapacity) // ConcurrentDictionary[TKey] static void Main() { // We know how many items we want to insert into the ConcurrentDictionary. // So set the initial capacity to some prime number above that, to ensure that // the ConcurrentDictionary does not need to be resized while initializing it. int NUMITEMS = 64; int initialCapacity = 101; // The higher the concurrencyLevel, the higher the theoretical number of operations // that could be performed concurrently on the ConcurrentDictionary. However, global // operations like resizing the dictionary take longer as the concurrencyLevel rises. // For the purposes of this example, we'll compromise at numCores * 2. int numProcs = Environment.ProcessorCount; int concurrencyLevel = numProcs * 2; // Construct the dictionary with the desired concurrencyLevel and initialCapacity ConcurrentDictionary cd = new ConcurrentDictionary(concurrencyLevel, initialCapacity); // Initialize the dictionary for (int i = 0; i < NUMITEMS; i++) cd[i] = i * i; Console.WriteLine("The square of 23 is {0} (should be {1})", cd[23], 23 * 23); } } Remarks For very large ConcurrentDictionary objects, you can increase the maximum array size to 2 gigabytes (GB) on a 64-bit system by setting the configuration element to true in the run-time environment. Note ConcurrentDictionary implements the IReadOnlyCollection and IReadOnlyDictionary interfaces starting with the .NET Framework 4.6; in previous versions of the .NET Framework, the ConcurrentDictionary class did not implement these interfaces. Like the System.Collections.Generic.Dictionary class, ConcurrentDictionary implements the IDictionary interface. In addition, ConcurrentDictionary provides several methods for adding or updating key/value pairs in the dictionary, as described in the following table. ConcurrentDictionary 提供以下功能: TryAdd() 新增一筆不存在的 TKey 資料, 若能成功新增, 則回傳 true. To do this: Add a new key to the dictionary, if it doesn't already exist in the dictionary Use this method: TryAdd Usage notes: This method adds the specified key/value pair, if the key doesn't currently exist in the dictionary. The method returns true or false depending on whether the new pair was added. TryUpdate() 修改一筆已存在的資料. To do this: Update the value for an existing key in the dictionary, if that key has a specific value Use this method: TryUpdate Usage notes: This method checks whether the key has a specified value, and if it does, updates the key with a new value. It's similar to the CompareExchange method, except that it's used for dictionary elements. dictionary[key] = newValue 設定 (Key 位置)等於(新值 newValue) To do this: Store a key/value pair in the dictionary unconditionally, and overwrite the value of a key that already exists Use this method: The indexer's setter: dictionary[key] = newValue AddOrUpdate() 新增或修改一筆資料 To do this: Add a key/value pair to the dictionary, or if the key already exists, update the value for the key based on the key's existing value Use this method: AddOrUpdate(TKey, Func, Func) // 若TKey不存在, 則以第1個Func處理, 否則以第2個Func處理. -or- AddOrUpdate(TKey, TValue, Func)// 同上, 但不提供第1個Func處理TKey不存在的情況. Usage notes: AddOrUpdate(TKey, Func, Func) accepts the key and two delegates. It uses the first delegate if the key doesn't exist in the dictionary; it accepts the key and returns the value that should be added for the key. It uses the second delegate if the key does exist; it accepts the key and its current value, and it returns the new value that should be set for the key. AddOrUpdate(TKey, TValue, Func) accepts the key, a value to add, and the update delegate. This is the same as the previous overload, except that it doesn't use a delegate to add a key. GetOrAdd() 讀取或新增一筆資料 To do this: Get the value for a key in the dictionary, adding the value to the dictionary and returning it if the key doesn't exist Use this method: GetOrAdd(TKey, TValue) -or- GetOrAdd(TKey, Func) Usage notes: These overloads provide lazy initialization for a key/value pair in the dictionary, adding the value only if it's not there. GetOrAdd(TKey, TValue) takes the value to be added if the key doesn't exist. GetOrAdd(TKey, Func) takes a delegate that will generate the value if the key doesn't exist. All these operations are atomic and are thread-safe with regards to all other operations on the ConcurrentDictionary class. The only exceptions are the methods that accept a delegate, that is, AddOrUpdate and GetOrAdd. For modifications and write operations to the dictionary, ConcurrentDictionary uses fine-grained locking to ensure thread safety. (Read operations on the dictionary are performed in a lock-free manner.) However, delegates for these methods are called outside the locks to avoid the problems that can arise from executing unknown code under a lock. Therefore, the code executed by these delegates is not subject to the atomicity of the operation.