查找在C#中线程被阻塞的原因

4
所以我有一个游戏,其中主线程执行您所期望的常规更新/渲染逻辑,第二个线程执行一些非常强烈的处理。我遇到的问题是,偶尔主线程会被中断,游戏会降低到60FPS以下。我相当确定它被另一个线程阻塞了,但由于没有显式锁定,我无法证明。
我能想到几种情况,导致主线程被次线程阻塞:
1. 第二个线程分配了大量小对象;内存分配会强制一个线程等待,而另一个线程则分配内存。这似乎不太可能发生,因为您期望在分配一个小对象之后,主线程可以继续分配所需的对象。 2. 一些JIT优化会防止次线程在执行时间过长时被中断。这毫无意义。 3. 某种正在被锁定的跨线程引用。这不太可能发生,因为该代码是有意通过队列进行分离的,在队列上,次线程拾取项目却不会锁定并阻止放置项目。 4. 操作系统的错误线程优先级,这也不太可能发生,因为这个问题在Linux和Windows上都会出现。
我尝试了加入秒表并测量哪些代码需要时间,但这并没有告诉我更多信息,只是"主线程在随机时间停止了500毫秒",它实际上并没有告诉我是否有锁定阻塞了主线程一段时间。
有什么技术可以用来缩小此问题的原因范围吗?
-----编辑-----
这些是运行Mono分析器并报告锁争用情况的结果:
Monitor lock summary
    Lock object 0x7f05190c9fe0: 1 contentions
            0.002126 secs total wait time, 0.002126 max, 0.002126 average
    1 contentions from:
            (wrapper runtime-invoke) object:runtime_invoke_void__this__ (object,intptr,intptr,intptr)
            System.Threading.Thread:StartInternal ()
            System.Threading.Timer/Scheduler:SchedulerThread ()
            (wrapper unknown) System.Threading.Monitor:FastMonitorEnterV4 (object,bool&)
            System.Threading.Monitor:Enter (object,bool&)
            System.Threading.Monitor:TryEnter (object,int,bool&)
            (wrapper managed-to-native) System.Threading.Monitor:try_enter_with_atomic_var (object,int,bool&)
    Lock object 0x7f051910b100: 1 contentions
            0.000628 secs total wait time, 0.000628 max, 0.000628 average
    1 contentions from:
            Ninject.Components.ComponentContainer:Get (System.Type)
            Ninject.Components.ComponentContainer:ResolveInstance (System.Type,System.Type)
            Ninject.Components.ComponentContainer:CreateNewInstance (System.Type,System.Type)
            System.Reflection.ConstructorInfo:Invoke (object[])
            System.Reflection.MonoCMethod:Invoke (System.Reflection.BindingFlags,System.Reflection.Binder,object[],System.Globalization.CultureInfo)
            System.Reflection.MonoCMethod:DoInvoke (object,System.Reflection.BindingFlags,System.Reflection.Binder,object[],System.Globalization.CultureInfo)
            System.Reflection.MonoCMethod:InternalInvoke (object,object[])
            (wrapper managed-to-native) System.Reflection.MonoCMethod:InternalInvoke (System.Reflection.MonoCMethod,object,object[],System.Exception&)
            (wrapper runtime-invoke) <Module>:runtime_invoke_void__this___object (object,intptr,intptr,intptr)
            Ninject.Activation.Caching.ActivationCache:.ctor (Ninject.Activation.Caching.ICachePruner)
            Ninject.Activation.Caching.GarbageCollectionCachePruner:Start (Ninject.Activation.Caching.IPruneable)
            (wrapper remoting-invoke-with-check) System.Threading.Timer:.ctor (System.Threading.TimerCallback,object,int,int)
            System.Threading.Timer:.ctor (System.Threading.TimerCallback,object,int,int)
            System.Threading.Timer:Init (System.Threading.TimerCallback,object,long,long)
            System.Threading.Timer:Change (long,long,bool)
            System.Threading.Timer/Scheduler:Change (System.Threading.Timer,long)
            (wrapper unknown) System.Threading.Monitor:FastMonitorEnterV4 (object,bool&)
            System.Threading.Monitor:Enter (object,bool&)
            System.Threading.Monitor:TryEnter (object,int,bool&)
            (wrapper managed-to-native) System.Threading.Monitor:try_enter_with_atomic_var (object,int,bool&)
    Lock object 0x7f05190ca000: 1 contentions
            0.000347 secs total wait time, 0.000347 max, 0.000347 average
    1 contentions from:
            (wrapper runtime-invoke) object:runtime_invoke_void__this__ (object,intptr,intptr,intptr)
            System.Threading.Thread:StartInternal ()
            System.Threading.Timer/Scheduler:SchedulerThread ()
            (wrapper remoting-invoke-with-check) System.Threading.EventWaitHandle:Reset ()
            System.Threading.EventWaitHandle:Reset ()
            (wrapper unknown) System.Threading.Monitor:FastMonitorEnterV4 (object,bool&)
            System.Threading.Monitor:Enter (object,bool&)
            System.Threading.Monitor:TryEnter (object,int,bool&)
            (wrapper managed-to-native) System.Threading.Monitor:try_enter_with_atomic_var (object,int,bool&)
    Lock contentions: 3
    Lock acquired: 3
    Lock failures: 0

这是游戏运行约20-30秒后的情况,我观察到至少有10次卡顿。在这段时间内,只有3次竞争锁的内容,并且所有的竞争都在16毫秒内解决。


将.NET CLR内存性能计数器放入Perfmon.exe中,检查是否存在相关性。 - Hans Passant
你正在使用Visual Studio吗?试试分析器和并发可视化工具(http://msdn.microsoft.com/en-us/library/dd537632.aspx)吧! - Ilian
我正在使用MonoDevelop在Linux上工作,因此无法访问这些工具。现在我正在使用Mono分析器检查锁争用情况,并在获取结果后更新问题。 - June Rhodes
你的代码中有 System.Threading.Timer。你是否通过 Invoke 从该计时器更新 UI?你是否在任何地方使用 Invoke() 来操作 UI? - Idle_Mind
你提到问题存在于Windows和Linux两个平台上。你的.NET框架版本是哪个? - cost
1个回答

4
您正在使用垃圾收集平台运行实时应用程序,从这一点上来说,您不能指望得到很多预测性。当垃圾回收系统在清理悬空对象时,它会锁定所有正在运行的线程,通过从根对象遍历“可跨越树”的3个“最近”的数据代数作为跨越树发现优化系统。但无论如何,在主线程中存在或不存在同步,都无法防止它不时地停止。
其次,呈现在交换函数(即直接3d术语中的“呈现到屏幕”)上是阻塞的,该函数等待“第三个最老帧完成呈现,最后一个呈现命令列表被刷新,并收到VSync信号”然后才允许您的主线程继续。您可以尝试在交换调用周围进行分析,以检查驱动程序是否与锁定有关。
第三,操作系统调度程序是抢占式的,每个内核滴答声长约1至15毫秒,您可以进行上下文切换。如果您拥有Linux版本比内核V 3.1更高(或相等),则将具有内核构建选项FULL_DYN_TICKS,该选项在整个系统上只有一个任务处于活动状态时禁用抢占式中断计时器,但在托管语言上,我觉得这个要求不太可能得到满足。但是,500ms代表非常长的时间,33个滴答声,只有当您有33个其他任务在同一时间以相同的优先级全速运行时才会发生。也不太可能。
您可能会因温度原因而限制CPU或GPU的硬件决策。
您可能拥有一个共享图形卡内存并泄漏的复合桌面,这迫使驱动程序不时地从主内存中交换纹理。这种错误在Linux上经常发生,特别是在像emerald、compiz之类的“危险”桌面上。
检查另一个3D应用程序并查看其行为,完全停止工作线程以查看它是否有助于主线程的正常运行。检查您的小对象分配和旧对象分配。第一代垃圾收集可能很难运行。
祝你好运

1
我认为你对这个问题是垃圾回收问题的看法是正确的。 我在每个更新步骤中添加了GC.Collect(0),可以有所减少延迟峰值,但我认为第二个线程分配了大量短暂的内存,导致垃圾处理需要在未来清理它。 - June Rhodes
1
结果发现我的程序使用了大量的内存,垃圾回收不得不频繁启动以将内存使用量降至2GB以下。 - June Rhodes
永远不要在游戏中使用32位。 - The Sharp Ninja

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接