再记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

时间:2024-01-12 09:11:38

在此之前项目有发生过两次类似的状况,都得以解决,但最近又会发现偶尔CPU会跑满,虽然之前使用过WinDbg解决过两次问题但人的记忆是不可靠的,今天处理同样问题的时候还是遇到了一些障碍,这一次希望可以记录的更全面些。

上两次的博文链接:记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)EntityFramework中的线程安全,又是Dictionary

首先请大家不要喷我,因为这一次还是关于Dictionary的一些低级错误,我自己看到都无语了。。。

抓取Dump

使用任务管理器抓取Dump,如果操作系统较低可以使用“Process Explorer”。

再记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

使用WinDbg分析

1.使用WinDbg打开dump文件。

2.加载sos.dll

命令:.loadby sos clr

再记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

3.查看相关线程信息

命令:!threads –special

special参数会将由CLR创建的特殊线程单独列出便于减少线程的排查工作。

再记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

红框圈出的是我们要重点排查的线程(工作者线程),至于其它的则是一些CLR自拥有的一些线程,如:GC线程、对象释放线程、计时器线程、I/O线程等。

线程类型的名称翻译:

  • GC:垃圾回收线程
  • Finalizer:对象释放线程,.Net至少有一个,用于专门处理对象释放。
  • Timer:计时器线程
  • ThreadpoolWorker:工作者线程
  • IOCompletion:I/O线程

4.查看具体线程堆栈

命令:~{ThreadId}s、!clrstack

~{ThreadId}s:将当前上下文切换到指定的线程内

再记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

!clrstack:得到当前的线程的堆栈信息

再记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

第二个红框的前两句太长了,我复制在下面:

000000d784afe180 000007fda1efa328 System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.Collections.Generic.KeyValuePair`2[[System.__Canon, mscorlib],[System.Boolean, mscorlib]], mscorlib]].FindEntry(System.__Canon)
000000d784afe1f0 000007fda1ef96eb System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.Collections.Generic.KeyValuePair`2[[System.__Canon, mscorlib],[System.Boolean, mscorlib]], mscorlib]].TryGetValue(System.__Canon, System.Collections.Generic.KeyValuePair`2<System.__Canon,Boolean> ByRef)

可以发现,是在TryGetValue方法时堵塞了,而看到红框中的最后一句则可以发现是EnumParseCacheHelper的Parse方法出了问题,这个方法主要是对枚举转换的一个缓存处理以提升性能。

为了再次确认问题,我继续对19、20、21、24等线程进行了查看,都是在这里堵塞了,那么问题浮出水面了,下面就去看代码,并且解决它。

解决问题

找到对应的代码:

再记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

问题显而易见,CacheDictionary是一个全局静态的字段,而我在下面方法使用它的时候丝毫没有注意并发下的情况,没有加锁来保证线程安全。。看到这感觉不可思议怎么犯这么低级的错误。。。

解决它方式:

再记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

解决方式很简单,使用了.NET4提供的线程安全的字典:ConcurrentDictionary。

关于这一次问题的思考

Dictionary为什么这么容易堵塞

这边引用之前的博文内容:

我知道Dictionary不是一个线程安全的类型,但我原本以为Dictionary在非线程安全方式下访问时数据会错乱,而不会堵塞或者死锁,而这次的这个问题让我感觉到讶异,为什么Add一个项目会造成堵塞?

反编译Dictionary的源码后发现异常的复杂,也没有细究,所以下面的一段描述大家抱有自己的想法去阅读,可能是错的也可能是对的。

再记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

再记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

上面是我认为存在问题的地方,当一个线程执行过Initialize后buckets数组的值被修改,而第二个线程同时进入了Initialize方法,那么第一个线程所维护的值被破坏,造成在算法环节出现了死循环,这也可以说明了为什么cpu有时候是50%有时候是99%的问题。

当前有多少个线程发生了这种状态,如果发生这种状态的线程越多则代表cpu占用越多。

这次问题的经验:以后在使用集合或字典时首先应该先想到System.Collections.Concurrent命名空间,虽然它的性能在正常情况下低于普通的Dictionary,但那么几十或者几百毫秒的损失对于稳定性来说微不足道,也减少了问题的发生。

写在最后

因为Rabbit.WeiXin是一个开源项目当然第一件事情就是发布更新。。避免更多的人出现此问题。

再记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

交流方式

QQ群:384413261(RabbitHub)

Email:majian159@live.com