c# - 游戏逻辑的最快迭代方法 : thread, 空闲处理程序、winproc 或其他我不知道的东西?

标签 c# multithreading performance

我正在创建一个新的 Windows C# 窗体应用程序,它使用各种硬件系统(Kinect V2 和 3D RFID 定位系统)。这是一个科学应用程序,而不是游戏,但具有绝对类似于游戏的逻辑(需要收集和管理 3d env 对象的物理数据,渲染图形 3D 环境等)。在大多数原生 Windows 环境游戏中,您可能有一个基本的主循环,它在监听来自 mse 和 kbd 的 DirectInput 时调用逻辑更新和渲染函数。但由于这是 C#,它更像是一个事件驱动的环境,我相信我需要依赖 3 个选项之一:

(1) 创建一个新线程并将其用作逻辑更新和 3d 渲染的主循环(当然要监听转义逻辑),或者
(2) 创建一个应用程序空闲处理函数,检查应用程序是否空闲,然后处理逻辑,或者
(3) 覆盖 winproc 并在那里执行我的类似游戏的逻辑。

所以我创建了一个测试应用程序:它有一个时钟显示,并使用刚才提到的所有三种方法为每种方法独立计算 FPS 计数器。这衡量每种方法每秒迭代的频率,并每秒显示一次在时钟旁边。

但是结果不是我所期望的。我认为新线程将是最快的方法,类似于主循环。但事实并非如此。 WinProc 被调用最多(即使将线程的优先级设置为最高)。此外,应用程序空闲处理程序是最慢的(正如预期的那样,它只在空闲时调用!) - 但我已经读到空闲处理是处理窗口 C# 窗体游戏类型应用程序的规范方法。这三个都使用完全相同的逻辑来计算它们的 FPS(只是一个 int,每次调用它们的方法时都会得到一个++)。

以下是应用运行几秒钟并稳定后的结果:

基于线程的 FPS:3,200
应用程序空闲处理程序 FPS:600
WinProc 覆盖 FPS:10,000

我知道为什么空闲处理程序是最慢的。但为什么线程的迭代频率低于 winproc?我在那里错过了什么?此外,在与窗体窗口交互时,winproc 发现每秒迭代次数大幅增加(正如预期的那样),但线程也是如此!为什么仅仅因为与 GUI 交互,独立线程就会被更频繁地调用?我显然不理解这里的一些基本知识 - 有人可以给我一些线索吗?

更新 1: 这是 self 的问题的第一个版本被搁置以来的一次编辑 - 显然人们希望看到一些实际的代码!这不是“我不知道为什么我的代码不起作用”——我试图梳理性能……所有这些都有效。所以这是基本代码 - 希望这会有所帮助!

谢谢!

代码:ClockText 变量只是 3 个文本框,在每个方法中用时间戳更新,而 UPS 变量是用每个方法计算的 FPS 更新的文本框:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices; //for DllImport of c++ functionality for win msg processing

namespace Clock1
{
    public partial class ClockForm : Form
    {
        //General app variables
        System.Threading.Thread t;
        bool KeepLooping = true;
        DateTime StartTime = DateTime.Now;
        DateTime StopTime = DateTime.Now;
        TimeSpan TimePassed = new TimeSpan();
        int FrameTick1 = 0;
        int FrameTick2 = 0;
        int FrameTick3 = 0;

        //Imported for use of c++ win msg variables

            //For message handler version
            [StructLayout(LayoutKind.Sequential)]
            public struct NativeMessage
            {
                public IntPtr Handle;
                public uint Message;
                public IntPtr WParameter;
                public IntPtr LParameter;
                public uint Time;
                public Point Location;
            }
            [DllImport("user32.dll")]
            public static extern int PeekMessage(out NativeMessage message, IntPtr window, uint filterMin, uint filterMax, uint remove);

            //Imported for winproc loop version processing on WM_PAINT (0x000F) messages (lowest allocation overhead)
            [DllImport("user32.dll")]
            public static extern int SendNotifyMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);  

        //Primary form loop
        public ClockForm()
        {
            //Loaded on program start

            //Init components for C# environment
            InitializeComponent();

            //For winproc method, override painting (double check the functionality of this):
            //SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);

            //Multithreaded method initialization:
            t = new System.Threading.Thread(MainLoop);
            t.Priority = System.Threading.ThreadPriority.Highest;
            t.Start();

            //Application idle event handling allocation:
            Application.Idle += HandleApplicationIdle;
        }

       //Multithreading method primary func
        public void MainLoop()
        {
            while (KeepLooping == true)
            {
                //Count Frames
                FrameTick1++;

                //Use Invoke because the new thread can't access UI elements directly
                MethodInvoker MI = delegate() 
                {
                    //Update time:
                    ClockText.Text = DateTime.Now.ToString("hh:mm:ss.fff tt");
                };
               ClockText.Invoke(MI);              
            }

            //When KeepLooping is no longer true, exit:
            Application.Exit();
            Environment.Exit(1);
        }

        //Application idle event handling method primary func
        void HandleApplicationIdle(object sender, EventArgs e)
        {
            while (IsApplicationIdle())
            {
                //Count Frames
                FrameTick2++;

                //Call frame update:
                UpdatesPerSecond();

                //Update time text
                ClockText2.Text = DateTime.Now.ToString("hh:mm:ss.fff tt");
            }
        }

        //Application idle event handling method secondary func
        bool IsApplicationIdle()
        {
            NativeMessage result;
            return PeekMessage(out result, IntPtr.Zero, (uint)0, (uint)0, (uint)0) == 0;
        }

        //Winproc method (override for the WndProc handler):
        protected override void WndProc(ref Message m)
        {
            //Updates with full FPS capacity (may be a CPU hit though, so consider doing this only on a certain event - maybe not WM_PAINT, but something else, a faster more regular heartbeat
            //NOte: All messages are sent to the WndProc method after getting filtered through the PreProcessMessage method.  Recursive infinite loop risk when working with windows control variables, due to updates and such

            //Count Frames
            FrameTick3++;

            //Update clock text:
            ClockText3.Text = DateTime.Now.ToString("hh:mm:ss.fff tt");

            //Proceed with regular message handling
            base.WndProc(ref m);
        }

        private void UpdatesPerSecond()
        {
            //Time Calc:
            StopTime = DateTime.Now;
            TimePassed = StopTime - StartTime;                                              
            if (TimePassed.Seconds >= 1)
            {
                //Update UPS/FPS count displays:
                UPS1.Text = FrameTick1.ToString("D");
                UPS2.Text = FrameTick2.ToString("D");
                UPS3.Text = FrameTick3.ToString("D");

                //Reset UPS count:
                FrameTick1 = 0;
                FrameTick2 = 0;
                FrameTick3 = 0;
                StartTime = DateTime.Now;
                TimePassed = TimeSpan.Zero;
            }
        }

        //Event handeler for exit button        
        private void btnExit_Click(object sender, EventArgs e)
        {
            //Detach event handler for idle event processing
            Application.Idle -= HandleApplicationIdle;

            //Stop the multi-threaded method
            KeepLooping = false;            
        }
    }
}

更新 2: 我想我会在采纳下面 Zer0 的建议(这是正确的!)后添加更新,以防有一天它可能对其他人有所帮助。正如他所提到的,嵌入在每个处理方法中的用于 GUI 更新的 Invoke 方法导致了瓶颈和奇怪的结果。结果好多了!!!

我将所有 GUI 更新重新定位到计时器功能,应用程序的结果如下所示:

enter image description here

最后的代码,以防其他人想自己测试:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices; //for DllImport of c++ functionality for win msg processing
using System.Globalization;

namespace Clock1
{
    public partial class VosClockForm : Form
    {
        //Timer for GUI updates:
        System.Windows.Forms.Timer MainTimer = new System.Windows.Forms.Timer();

        //General app variables
        System.Threading.Thread t;
        bool KeepLooping = true;
        bool ExitApplicationNow = false;
        DateTime StartTime = DateTime.Now;
        DateTime StopTime = DateTime.Now;
        TimeSpan TimePassed = new TimeSpan();
        CultureInfo StrictCulture = CultureInfo.InvariantCulture;
        long MethodTick1 = 0;
        long MethodTick2 = 0;
        long MethodTick3 = 0;
        long FrameTick = 0;

        //Imported for use of c++ win msg variables

            //For message handler version
            [StructLayout(LayoutKind.Sequential)]
            public struct NativeMessage
            {
                public IntPtr Handle;
                public uint Message;
                public IntPtr WParameter;
                public IntPtr LParameter;
                public uint Time;
                public Point Location;
            }
            [DllImport("user32.dll")]
            public static extern int PeekMessage(out NativeMessage message, IntPtr window, uint filterMin, uint filterMax, uint remove);

            //Imported for winproc loop version processing
            [DllImport("user32.dll")]
            public static extern int SendNotifyMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);  

        //Primary form loop
        public ClockForm()
        {
            //Loaded on program start

            //Init components for C# environment
            InitializeComponent();

            //If overriding winproc based painting, need to tell system we are doing the drawing
            //SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);

            //Multithreaded method initialization:
            t = new System.Threading.Thread(MainLoop);
            t.Priority = System.Threading.ThreadPriority.Normal;
            t.Start();

            //Application idle event handling allocation:
            Application.Idle += HandleApplicationIdle;

            //Start the MainTimer for updates:
            MainTimer.Tick += new EventHandler(MainTimerEvent);
            MainTimer.Interval = 15; //15 ms = 67 fps max, may need to tweak this a bit!
            MainTimer.Start();
        }

        //Multithreading method primary func (don't post GUI updates using Invoke here as it causes a huge bottleneck, holding up the mainloop to wait for a graphics update
        public void MainLoop()
        {
            while (KeepLooping == true)
            {
                //Count Frames
                MethodTick1++;
            }

            //When KeepLooping is no longer true, exit:
            ExitApplicationNow = true;
        }

        //Application idle event handling method primary func
        void HandleApplicationIdle(object sender, EventArgs e)
        {
            while (IsApplicationIdle())
            {
                //Count Frames
                MethodTick2++;
            }
        }

        //Application idle event handling method secondary func
        bool IsApplicationIdle()
        {
            NativeMessage result;
            return PeekMessage(out result, IntPtr.Zero, (uint)0, (uint)0, (uint)0) == 0;
        }

        //Winproc method (override for the WndProc handler): All messages are sent to the WndProc method after getting filtered through the PreProcessMessage method.  Recursive infinite loop risk when working with windows control variables, due to updates and such
        //Do NOT update any GUI elements in this function!
        protected override void WndProc(ref Message m)
        {
            //Count Frames
            MethodTick3++;

            //Check for application exit (consider moving this to whatever function ends up handling periodic GUI updates and such)
            if (ExitApplicationNow == true)
            {
                //Detach event handler for idle event processing
                Application.Idle -= HandleApplicationIdle;

                //Exit:
                Application.Exit();
                Environment.Exit(1);
            } 

            //Proceed with regular message handling
            base.WndProc(ref m);
        }

        private void UpdateInterface() 
        {
            //Update Clock:
            lblClock.Text = DateTime.Now.ToString("hh:mm:ss.fff tt");

            //GUI Update tick:
            FrameTick++;

            //UPS Calc:
            StopTime = DateTime.Now;
            TimePassed = StopTime - StartTime;                                              
            if (TimePassed.Seconds >= 1)
            {
                //Update GUI FPS
                lblGuiFps.Text = FrameTick.ToString("###,###,###,###", StrictCulture);

                //Update UPS/FPS count displays:
                lblUPS1.Text = MethodTick1.ToString("###,###,###,###", StrictCulture);
                lblUPS2.Text = MethodTick2.ToString("###,###,###,###", StrictCulture);
                lblUPS3.Text = MethodTick3.ToString("###,###,###,###", StrictCulture);

                //Reset UPS count:
                MethodTick1 = 0;
                MethodTick2 = 0;
                MethodTick3 = 0;
                FrameTick = 0;
                StartTime = DateTime.Now;
                TimePassed = TimeSpan.Zero;
            }
        }

        //Event handeler for exit button        
        private void btnExit_Click(object sender, EventArgs e)
        {   
            //Stop the multi-threaded method
            KeepLooping = false;            
        }

        //Timer Event Handling: GUI Updates and related updates that don't require more than 60fps
        private void MainTimerEvent(Object myObject, EventArgs myEventArgs)
        {
            //Call frame update
            UpdateInterface();
        }
    }
}

最佳答案

这三种方法中最快的是独立的Thread

您的单独线程基准测试存在根本缺陷。它是 ClockText.Invoke(MI)。原因是 Invoke 在 GUI 更新完成之前阻塞。这非常慢。

如果您不想阻止,可以改用 BeginInvoke,但我也不推荐这样做。

当谈到高性能时,我会强烈避免“推送”到 GUI 线程(通过 InvokeBeginInvokeSynchronizationContext 等...)。这些都以相同的方式运行 - 它们使用 Windows 消息队列,就像您的 WndProc 解决方案所做的那样。你可以淹没那个队列。这是响应用户输入和无数其他事物(如绘画消息)的同一个队列。

相反,我建议从 GUI 线程“拉取”。有很多不同的方法可以做到这一点,所以我不能在不了解更多的情况下给你写一些具体的东西。但一个简单的方法是使用 System.Windows.Forms.Timer 并经常刷新 GUI。

关于c# - 游戏逻辑的最快迭代方法 : thread, 空闲处理程序、winproc 或其他我不知道的东西?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25690359/

相关文章:

java - 我所有的按钮点击方法在操作之前等待线程完成

php - 使用PHP限制下载速度

c++ - 如何在 Linux 上查看(C 和 C++)二进制符号?

c# - 在代码后面的 gridview 事件处理程序中切换 DIV 可见性不起作用

c# - .NET 单位类,英寸到毫米

java - 当作业到达 threadPoolExecution 时调用所有线程

c++ - 多线程编程

java - Android引起的java.lang.outofmemory错误

c# - 用数据读取器填充业务对象的最快方法?

c# - 菜单快捷键范围