Starting a new thread makes MonoGame skip Draw() and only do Update() for a number of frames

Just after I create a task (System.Threading.Tasks.Task) Draw() is skipped and only Update() is called for a number of frames (lag) even though I timed the call itself and it only takes ~0.3ms. Doesn’t matter what logic is in the delegate. This also happens if I create a thread once I start it. I run this code at any point and it will skip a few draws (happens both in Debug and Release mode).

Task.Run(() => { });

The same happens with this:

var thread = new Thread(() => { });
thread.Start();

I am using FixedTimeStep=true (although there is also a delay with IsFixedTimeStep=false) with 144 FPS.
This is on MonoGame 3.8.0 DesktopGL with Windows 10.

Does anyone know what is causing this and how to fix it? Thanks.

I did some testing. I only observe this behavior (frame drops) the first few times I execute Task.Run.... Maybe .NET has to initialize some stuff the very first time this is run or some such? Odd.

Test code:

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;


namespace TestTaskFrameDrop
{
	public class Game1 : Game
	{
		private GraphicsDeviceManager _graphics;


		public Game1()
		{
			_graphics = new GraphicsDeviceManager(this);
			Content.RootDirectory = "Content";
			IsMouseVisible = true;

			TargetElapsedTime = TimeSpan.FromSeconds(1f / 144f);
			IsFixedTimeStep = true;

			_graphics.SynchronizeWithVerticalRetrace = false;
		}


		protected override void Update(GameTime gameTime)
		{
			Debug.WriteLine("Update - " + Math.Round(1f / gameTime.ElapsedGameTime.TotalSeconds));

			if (Keyboard.GetState().IsKeyDown(Keys.Space))
				for (int i = 0; i < 100; i++)
					Task.Run(() => { });

			base.Update(gameTime);
		}


		protected override void Draw(GameTime gameTime)
		{
			Debug.WriteLine("Draw");

			GraphicsDevice.Clear(Color.CornflowerBlue);

			base.Draw(gameTime);
		}
	}
}

When running this I get solid 144fps/hz, then when I press spacebar it drops for about 10 frames or so, then returns steady whether holding space or not.

Yes, I observed similar behavior. It is only the first few times.
It only took ~0.3 milliseconds for that Task.Run (and about the same duration each time). The time it took to execute Update overall didn’t change either and neither did ElapsedGameTime in Update. But maybe there is some work done between Update calls or it messes with how MonoGame detects when it should do frame-skipping somehow (don’t know how that works, maybe I will look at the source code).

Thank you for putting the test code together!

What happens if you creat a new thread the old way?

It does not happen until I start the thread with thread.Start(), then it happens.

var thread = new Thread(() => { }); // Nothing happens
thread.Start(); // Triggers frame-skipping

This will trigger the frame-skipping every time not just the first few times a new thread is started. Starting a thread task probably uses up the thread pool at some point and it stops starting new threads, so starting a new thread is probably the source of the problem.

Forgive me, i didnt see the bit at the end if your OP.

Not going to get chance today, but ill see uf i can recreat it and come up with a work around.

No worries, I edited the post.
After some more testing, I can see 1 frame passes and then there is a large gap in time between the Draw call and the next Update call (starting the new thread must be doing some heavy work in the background that is blocking the game loop) and then the game loop tries to catch up with the time passed by calling Update a bunch of times.

So starting a new thread is maybe just slow, not sure what to do about that, although it is strange that would block the main thread.

Have a list if threads/Tasks pre instanciated, then call Start/Run when you need it. See if its the creation of the thread or it initial execution

Im guessing its the creation.

Back in the XNA days i think i wrote a thread manager to do that. Ill see if i can find my old blog post. It covered threading on the XBox too as you only have access to set cores lol

It is thread.Start() that is causing it like I said, but yeah dirty workaround is to create max number of threads you need and start them at initialization, then make them wait until you need them with semaphores. Just kind of silly to create your own thread pool when Task.Run is supposed to manage that for you.

I also noticed if I start a bunch of threads in the same Update it does not affect how long the game loop is blocked compared to starting just one.

Why do you need so many threads?

I did not say I needed “so many” threads. I do pathfinding on a separate thread, so right now I create a task for each pathfinding call, so if there is another pathfinding call while the one thread is busy, I may want to open up another thread rather than wait for that one to finish (this resource juggling is handled by the thread pool, not me).

If it happens only the first few times then presumably this is just a result of the .NET Just-In-Time compiler, after the JIT compiler has compiled the Task.Run command to machine code it’ll be ready to execute quickly after that without causing lag.

I think I would just ignore this personally as it doesn’t sound like this is an issue execpt the first times. If this is a big deal anway I suppose a workaround would be to run a dummy _ = Task.Run(() => {}) anywhere before the first tick happens as a “warm up” strategy.

The issue is also present in the newest nuget package MonoGame version 3.8.1.303.
However, if I build from source (newest version of MonoGame repo) and instead use a reference to that project the issue is resolved. So either they have fixed it since 3.8.1.303 or the issue is with the nuget packages (maybe building using Windows does the trick?). This is very frustrating because it means I cannot debug the issue in the game loop source code, since it is resolved when I build from source.

The MonoGame.Framework.DesktopGL package was last updated on 26-07-2022 according to nuget gallery.

This happens every time when starting a new thread the old way, so it is probably not the JIT compiler. The only workaround is creating the threads you need at initialization.

That should be because creating new threads manually with new Thread(...) are expensive since they don’t get the threads from the .NET Thread Pool it’ll actually make a brand new thread with associated overhead. Task.Run() will get an already created thread from the Thread Pool however.

It is expensive, but not THAT expensive. I benchmarked it to 0.4 milliseconds and the delay is like 70 milliseconds and happens a frame after starting a thread (with thread.Start()). Also then how come it is solved when I build from source?

Have you tested the debug/release version by clicking on the .exe from explorer?
…or by starting it without debugging (Ctrl+F5)?

Yes, like I stated in my OP, it happens both in Debug and Release mode. However, I now notice when building from source it only works in Debug and not in Release (the delay is only in Release mode).

If I compile from source using Release it gives the delay, if I do it using Debug it does not. This aligns with the fact that the nuget package is probably compiled in Release mode.

If I build from source in release mode I can see Game.Tick() → DoDraw() → EndDraw() → -> GraphicsContext.SDL.SwapBuffers() → SDL.GL.SwapWindow() is what is holding things up every time (measured with Stopwatch). However, this is a function that can take a while (a few milliseconds), so it could just be something on another thread is more likely to happen while that function is executing.

A simple way to test this out is to override Game.EndDraw() in your class that inherits from Game

protected override void EndDraw()
{
}

That way SDL.GL.SwapWindow() will not get called and this means the delay is no longer there. Success at the small cost of displaying no graphics!

I am completely stumped as to what is causing this and I have to concede defeat and only start my threads on initialization.

if you time Sleep(1) you will get a range of 1 to 16 ms in windows by default thats a frame drop. 16ms is 1/60 sec. So the threading stuff probably usings it and thats the default for timers.

on older version its global, meaning if you ran Chrome, timers would work because they set it . this bug can take weeks to deal with so here you go, its not mentioned. not saying it will fix your problem but eventually in a game with any complexity will have 2 - 3 threads… you have 4- 8 processors so use them.

i need it and almost every game runing needs it and in your case you are using threads and you suspected another thread… well that may have a lock somewhere

[System.Runtime.InteropServices.DllImport(“winmm.dll”, EntryPoint = “timeBeginPeriod”)]
** public static extern uint timeBeginPeriod(uint uMilliseconds);**

try timeBeginPeriod(4)… it will affect only what is in your process after a certain vresion of windows…

in you startin code timeBeginPeriod(4) // close enough , 1 is too fine and drains battery or heat up the system.

//do this on exit, in case of earlier versoin of windows.
[DllImport(“winmm.dll”, EntryPoint = “timeEndPeriod”, SetLastError = true)]
public static extern uint TimeEndPeriod(uint uMilliseconds);




  took me weeks to mess with then I find (its a secret by some people who dont like to share or dont throw up a "wall of text"   like i do.  to save you weeks for reading my "lifes story:" as someone called it, take it or leave it.

 and then the locks work.  they depend on the system timer. 1ms  it will draiin the battery,  i've mentioned this before, its the secret to smooth framerates, there might be other ways i dont care, and then for android i cant afford to do locks at all i copy .. and stay off the ui tread.

 and i dont know android and mac, the specific call..but its in the web somewhere. people dont like to share it because it cost them a lot of time ..   you simple need to know the secret and get lucky googling it. of do the test of Sleep(1) as i did and then infer that is a sort of polling 
  my gift to the few toxic ego people who erased my helpful comments . probably so talentless they need to keep secrets.    can't take a joke and are prettyu disrespectful to very smart people who might not be perfectly sane..or might have a nervous disorder .   

enjoy .   life is too short to be stuck on a problem. 

  there and good people on this thread and i fyou dont know this trick you basically very limited..  especially wiht a physics thread.. it might pile or go to 5000 fsp and the timer in monogame last i looked is useless.  its game class is documented that you can and probably should make your own game loop..

would you like me to fix the typos and then make it perfectly concise, moderators.. ?  you konw who you are and why you shoud be nicer to  peopole different than normal people.