Stop Making Threads –
Moonside Games
Stop Making Threads
<br>2025-06-03
A folk definition of insanity is to do the same thing over and over again and to expect the results to be different. By this definition, we in fact require that programmers of multithreaded systems be insane. Were they sane, they could not understand their programs. -Edward A Lee, “The Problem with Threads”
A common question about SDL3 is: “How do I use multi-threading with the GPU API?”
In some sense, this is a reasonable question. One of the main motivations for the development of Vulkan and other modern graphics systems was that OpenGL and Direct3D11 contexts were not thread-safe at all. And more threads = more efficiency, right? That said, if you are asking such an open-ended question, it means you have a solution in search of a problem. This is a fundamentally wrong question to ask. Threads are not free performance enhancers. They are expensive to start up and tear down, and they enormously increase the complexity of the code base and the amount of mental effort you have to expend to reason about data integrity. Threads need to be used carefully.
Since I’ve grabbed your attention with this provocative headline, I have to admit: I do use threads in my applications. But I have specific and considered reasons for doing so.
Before we get started: this advice is targeted at your average solo developer or small team shipping games with SDL. If you are a professional working on advanced cutting-edge techniques, this advice does not apply to you.
Basic principles of threading
A thread is an independent execution sequence within a single process. Almost all CPUs nowadays have multiple cores, so using threads increases the CPU utilization. Each thread maintains its own stack, but it has a shared heap with all other threads. On the one hand this makes communication between threads easy, because they all access the same heap memory. On the other hand, this introduces a new kind of memory error: a race condition . Threads run in an unpredictable order relative to each other, and since they are accessing shared memory, that memory can be manipulated in unpredictable ways.
To address this, we can introduce locks which prevent multiple threads from accessing memory at the same time. If two threads try to acquire a lock at the same time, one thread must wait for the other to release the lock. This can introduce the problem of deadlocks , which is when two processes are each holding on to a lock that the other needs to proceed. Both threads are stopped and the program locks up.
When you introduce threads into your program, you need to be careful to avoid concurrency issues like race conditions and deadlocks. These issues can be subtle and extremely difficult to reproduce and debug.
The GPU API provides certain specific thread-safety guarantees. The intended threading workflow is to acquire a command buffer, record commands into it, and submit it all on the same thread. As long as you stick to that workflow, you won’t have any inconsistencies using threads.
An obvious performance principle here is that threads only increase performance if they are actually doing something. If a thread is waiting for something to happen on another thread, it is not increasing the CPU utilization of the program. Keep this in mind as we continue.
Game Application Flow
This is the structure of a typical SDL main thread game loop:
Handle SDL events until the event queue is empty.
Run the game’s update logic.
Run the game’s rendering logic.
Some kind of sleep, whether it’s frame pacing logic or waiting for vblank.
This is pretty straightforward. Some have asked: What if I could squeeze out a few extra cycles by having a render loop on a render thread?
Don’t use a render thread
The truth is that this has always been a bad idea. I’ve lost track of how many reported issues have chalked up to “someone called OpenGL on a thread”. But it’s just as much of a bad idea as it’s always been even if SDL GPU has certain thread safety guarantees.
I went into detail in one of my other posts about how the GPU is on a separate execution timeline from the CPU. In other words, the processing of GPU commands is already asynchronous, which immediately invalidates one of the primary benefits of using a thread.
In SDL GPU and APIs like it, this asynchronicity is very explicit. You insert commands into a command buffer on the CPU, and submit that command buffer to the GPU when you are ready for those commands to begin executing. In the early days of Vulkan, there were worries that command buffer recording would be relatively expensive. These fears have not borne out whatsoever. Command recording is cheap, and you don’t have to worry about the overhead. As scientist Gene Amdahl once helpfully stated:
“The overall performance improvement gained by optimizing a single part...