例如:
ConcurrentDictionary<string,Payload> itemCache = GetItems();
foreach(KeyValuePair<string,Payload> kvPair in itemCache)
{
if(TestItemExpiry(kvPair.Value))
{ // Remove expired item.
Payload removedItem;
itemCache.TryRemove(kvPair.Key, out removedItem);
}
}
显然,对于普通的Dictionary,这将引发异常,因为删除项会在枚举的生命周期中更改字典的内部状态。我的理解是,并非ConcurrentDictionary的情况,因为提供的IEnumerable处理内部状态更改。我明白了吗?有没有更好的模式可供使用?
答案 0 :(得分:33)
我很奇怪你现在收到的两个答案似乎证实你不能这样做。我只是自己测试了它并且运行良好而没有任何例外。
下面是我用来测试行为的代码,接下来是输出的摘录(当我按下'C'以清除foreach
中的字典和S
之后立即停止背景线程)。请注意,我对这个ConcurrentDictionary
:16个线程计时器施加了相当大的压力,每个计时器大约每15毫秒尝试添加一个项目。
在我看来,这个类非常强大,如果你在多线程场景中工作,值得你注意。
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace ConcurrencySandbox {
class Program {
private const int NumConcurrentThreads = 16;
private const int TimerInterval = 15;
private static ConcurrentDictionary<int, int> _dictionary;
private static WaitHandle[] _timerReadyEvents;
private static Timer[] _timers;
private static volatile bool _timersRunning;
[ThreadStatic()]
private static Random _random;
private static Random GetRandom() {
return _random ?? (_random = new Random());
}
static Program() {
_dictionary = new ConcurrentDictionary<int, int>();
_timerReadyEvents = new WaitHandle[NumConcurrentThreads];
_timers = new Timer[NumConcurrentThreads];
for (int i = 0; i < _timerReadyEvents.Length; ++i)
_timerReadyEvents[i] = new ManualResetEvent(true);
for (int i = 0; i < _timers.Length; ++i)
_timers[i] = new Timer(RunTimer, _timerReadyEvents[i], Timeout.Infinite, Timeout.Infinite);
_timersRunning = false;
}
static void Main(string[] args) {
Console.Write("Press Enter to begin. Then press S to start/stop the timers, C to clear the dictionary, or Esc to quit.");
Console.ReadLine();
StartTimers();
ConsoleKey keyPressed;
do {
keyPressed = Console.ReadKey().Key;
switch (keyPressed) {
case ConsoleKey.S:
if (_timersRunning)
StopTimers(false);
else
StartTimers();
break;
case ConsoleKey.C:
Console.WriteLine("COUNT: {0}", _dictionary.Count);
foreach (var entry in _dictionary) {
int removedValue;
bool removed = _dictionary.TryRemove(entry.Key, out removedValue);
}
Console.WriteLine("COUNT: {0}", _dictionary.Count);
break;
}
} while (keyPressed != ConsoleKey.Escape);
StopTimers(true);
}
static void StartTimers() {
foreach (var timer in _timers)
timer.Change(0, TimerInterval);
_timersRunning = true;
}
static void StopTimers(bool waitForCompletion) {
foreach (var timer in _timers)
timer.Change(Timeout.Infinite, Timeout.Infinite);
if (waitForCompletion) {
WaitHandle.WaitAll(_timerReadyEvents);
}
_timersRunning = false;
}
static void RunTimer(object state) {
var readyEvent = state as ManualResetEvent;
if (readyEvent == null)
return;
try {
readyEvent.Reset();
var r = GetRandom();
var entry = new KeyValuePair<int, int>(r.Next(), r.Next());
if (_dictionary.TryAdd(entry.Key, entry.Value))
Console.WriteLine("Added entry: {0} - {1}", entry.Key, entry.Value);
else
Console.WriteLine("Unable to add entry: {0}", entry.Key);
} finally {
readyEvent.Set();
}
}
}
}
cAdded entry: 108011126 - 154069760 // <- pressed 'C'
Added entry: 245485808 - 1120608841
Added entry: 1285316085 - 656282422
Added entry: 1187997037 - 2096690006
Added entry: 1919684529 - 1012768429
Added entry: 1542690647 - 596573150
Added entry: 826218346 - 1115470462
Added entry: 1761075038 - 1913145460
Added entry: 457562817 - 669092760
COUNT: 2232 // <- foreach loop begins
COUNT: 0 // <- foreach loop ends
Added entry: 205679371 - 1891358222
Added entry: 32206560 - 306601210
Added entry: 1900476106 - 675997119
Added entry: 847548291 - 1875566386
Added entry: 808794556 - 1247784736
Added entry: 808272028 - 415012846
Added entry: 327837520 - 1373245916
Added entry: 1992836845 - 529422959
Added entry: 326453626 - 1243945958
Added entry: 1940746309 - 1892917475
另请注意,基于控制台输出,看起来foreach
循环锁定了试图向字典添加值的其他线程。 (我可能错了,但除此之外我猜你会在“COUNT”行之间看到一堆“添加条目”。)
答案 1 :(得分:14)
只是为了确认the official documentation明确声明它是安全的:
从字典返回的枚举器可以安全使用 同时读取和写入字典,但它确实如此 不代表字典的即时快照。该 通过枚举器公开的内容可能包含所做的修改 在调用GetEnumerator之后到字典。
答案 2 :(得分:2)
有关此行为的其他信息,请访问:
段:
- 最大的变化是我们正在迭代“Keys”属性返回的内容,该属性返回给定点字典中键的快照。这意味着循环不会受到对字典的后续修改的影响,因为它在快照上运行。在没有涉及太多细节的情况下,迭代集合本身具有完全不同的行为,可以允许后续修改包含在循环中;这使得它更不确定。
- 如果在循环开始后其他线程添加了项目,它们将存储在集合中,但它们不会包含在此更新操作中(递增计数器属性)。
- 如果在调用TryGetValue之前某个项目被另一个线程删除,则该调用将失败并且不会发生任何事情。如果在调用TryGetValue后删除了某个项目,则为“tmp。
答案 3 :(得分:0)
编辑后,检查丹涛解决方案并独立测试。
是的,是简短的回答。它不会除外,它似乎使用细粒度锁定,并按照广告的方式工作。
鲍勃。