Hashtable 集合已修改可能无法执行枚举操作

最近项目中出现一个幽灵问题,站点大部分时间都是正常的,但是会在不确定的情况下突然挂掉,由于之前没有加日志,所以一直没找到原因,现在加了日志,问题就找到了。原因是有个地方抛了一个异常: 集合已修改;可能无法执行枚举操作。在System.Collections.Hashtable.HashtableEnumerator.MoveNext()

出现问题的代码可以抽象为如下代码:

public class MvcApplication : HttpApplication
{
private static Hashtable _cache = new Hashtable();

public override void Init()
{
foreach (var key in _cache.Keys)
{
//这里是一些耗时的操作
}

base.Init();
}
}

可以看出,问题就是出现在了遍历_cache.Keys的时候_cache里面的Keys被另一个线程修改了所导致的。所以,解决办法之一就是在遍历_cache.Keys之前先把_cache.Keys复制一份,然后再遍历这个复制后的副本。

所以第一种解决办法如下:

public class MvcApplication : HttpApplication
{
private static Hashtable _cache = new Hashtable();

public override void Init()
{
var keys = _cache.Keys.Cast<String>().ToList();
foreach (var key in keys)
{
//这里是一些耗时的操作
}

base.Init();
}
}

那么究竟是为什么会出现遍历的时候被别的线程修改了集合呢?原来,IIS 会定期对资源进行释放操作,而此操作将导致站点重启,站点重启导致初始化代码会多次重复执行,如果站点重启时恰好有其他请求来遍历集合,就会发生这个问题。

还有一种解决办法,就是用并发集合类ConcurrentDictionary 。如果_cache里面的元素非常多的时候,用这种方式将会得到最高的性能。因为项目中的_cache不会有太多元素,所以这里我就懒得啰嗦了。

虽然Hashtable在多个线程读一个线程写的情况下是线程安全的,但是所谓的线程安全也仅仅是只有一个线程写的时候,在这个时候允许应用程序以无锁的模式读取数据,而且读是通过Key去读,而不是通过遍历去读。如果是要通过遍历读取Hashtable里面的元素,那就不是线程安全的了,如果你在遍历集合元素的时候,另外一个线程又来修改集合,那么枚举器就会抛异常了,如果想要不抛异常,就需要捕捉异常或者加锁了,但这不是一种好的处理方式。所以,建议采用副本或并发集合类来处理。


按说这种低级错误不应该是我犯的,可我就是犯了,如果非要找个借口的话,可能跟要走了没心思干活有点关系吧 ...