了解线程在 C# 中被阻塞的原因

本文关键字:线程 了解 | 更新日期: 2023-09-27 18:33:02

所以我有一个游戏,主线程执行您期望的通常更新/渲染逻辑,第二个线程执行一些非常密集的处理。 我遇到的问题是偶尔主线程会中断,游戏会降至 60FPS 以下。 我相当确定它被另一个线程阻止了,但由于没有显式锁定,我无法证明这一点。

我可以想到几种情况来解释为什么主线程会被辅助线程阻塞:

    第二个线程分配
  • 大量小对象;内存分配强制一个线程等待,而另一个线程分配内存。 这似乎不太可能,因为您希望在分配一个小对象后,主线程可以继续分配它需要的内容。
  • 某种形式的 JIT 优化,可防止辅助线程在花费太长时间时中断。 这完全没有意义。
  • 某种被锁定的跨线程引用。 不太可能,因为代码被队列有意分隔,其中辅助线程从队列中选取项目,但不锁定和阻止将项目放入队列。
  • 操作系统的线程优先级不好,也不太可能,因为此问题在 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 毫秒的时间来解决。

了解线程在 C# 中被阻塞的原因

您正在使用垃圾回收平台来运行实时应用程序,因此不能期望从这一点上获得太多预测性。垃圾回收系统在空间不足时锁定所有正在运行的线程,通过遍历"从根对象遍历可跨度树"来清理悬空对象,使用 3 代"最近"数据作为生成树发现优化系统。但无论如何,主线程中是否存在同步都不会阻止它不时停止。

其次,渲染在交换函数(直接 3D 术语中为"呈现到屏幕"(上阻塞,这是一个等待"第三个最旧的帧完成渲染、刷新最后一个渲染命令列表并接收 VSync 信号"的函数,然后它允许你的主线程继续。您可以尝试围绕交换调用进行分析,以检查驱动程序是否与您的锁定有关。

第三,操作系统调度程序,就像你提到的,是抢占式的,每个内核滴答,即 1 到 15 毫秒之间,你可以切换上下文。如果您的 linux 比内核 V 3.1 更新(或等于(,您将拥有内核构建选项FULL_DYN_TICKS当整个系统上只有一个任务处于活动状态时禁用抢占式中断计时器,但在托管语言上,我觉得这个要求不太可能得到满足。但是,500ms 代表一个很长的时间,33 个时钟周期,这只有在您有 33 个其他任务同时以相同的频率运行完整的 CPU 时才会发生。也不太可能。

出于温度原因,您可以决定硬件

来限制 CPU,或者出于此原因限制 GPU。

你可能有一个共享显卡内存并泄漏的复合桌面,这会迫使驱动程序不时交换主内存中的纹理。这种错误在Linux上经常发生,尤其是在"危险"的桌面上,如emerald,compiz等。

检查另一个 3D 应用程序并查看行为,完全停止您的工作线程,看看它是否有助于主线程的健全性。检查小对象分配和旧对象分配。第一代垃圾回收的运行可能很重。

祝你好运