So I got single threaded game logic done and working in my component game engine ,but this is 2011, we use multithreading like a boss. So I need some help to get started from people experienced in this field.
I know how multithreading works and I've experimented with it before so I'm not a complete beginner.
The main question I'm asking is: from experience in creating game engines, what should have its own thread and what should be kept within the main thread?
Also, do you know some good articles that talks about multithreading in games?
Multitreading, what to and what not to thread?
Essentially things that should go in their own thread are activities that can run parallel to each other where there are no concurrency issues or they can be managed safely.
A good example is sound; the playing of music in your game would live in its own thread. Other tasks that run in the background are also good candidates. For instance you might have seamless level loading task that only keeps objects and textures within a certain radius of the player in memory.
A good example is sound; the playing of music in your game would live in its own thread. Other tasks that run in the background are also good candidates. For instance you might have seamless level loading task that only keeps objects and textures within a certain radius of the player in memory.
Two popular ways of multi-threading are module based and task based. In module based multi-threading, you taking some large modules of your game and get them to run in multiple threads. Examples might include Rendering, Physics, AI, Pathfinding, Game Logic, Sound etc. In task based, your program is split into many tasks that can be executed in parallel. For example, finding a particular path might be a task, or calculating the physics for some subset of the world. or managing sound for frame N. You spawn a number of threads (approximately equal to the number of logical cores available) and each thread requests tasks from the task pool. This acts like a simple operating system kernel.
The advantage of the former is the simplicity. Taking a particular module and protecting it is conceptually easy. However, it doesn't scale as well, as you can only ever use N cores, where N is the number of subsystems you have threaded.
The task based is more scalable, but far more difficult as you effectively need to protect each task, which is tricky to get right. An example is a "double buffered" approach, during frame X each task can access a read only copy of frame X - 1's data, but select tasks can write to select areas of frame X's data. So your AI and pathfinding might be using data that is "out of date", and your physics system might be writing to the game object data. A lot of care needs to be taken to ensure that everyone is reading and writing correctly, and that you're not causing synchronisation bugs or liveness issues. You can scale to any number of cores if you get it right though.
It really depends on the game. If you have one massive subsystem that is soaking up your processor time, then using a 2 thread system might be enough to win the day, and far simpler than breaking your entire game into lots of pieces. Don't over-engineer the game, nor should you multi-thread for the sake of it.
The advantage of the former is the simplicity. Taking a particular module and protecting it is conceptually easy. However, it doesn't scale as well, as you can only ever use N cores, where N is the number of subsystems you have threaded.
The task based is more scalable, but far more difficult as you effectively need to protect each task, which is tricky to get right. An example is a "double buffered" approach, during frame X each task can access a read only copy of frame X - 1's data, but select tasks can write to select areas of frame X's data. So your AI and pathfinding might be using data that is "out of date", and your physics system might be writing to the game object data. A lot of care needs to be taken to ensure that everyone is reading and writing correctly, and that you're not causing synchronisation bugs or liveness issues. You can scale to any number of cores if you get it right though.
It really depends on the game. If you have one massive subsystem that is soaking up your processor time, then using a 2 thread system might be enough to win the day, and far simpler than breaking your entire game into lots of pieces. Don't over-engineer the game, nor should you multi-thread for the sake of it.
reply
Do you have any tips for the synchronisation issues you mentioned? Take for example synchronizing rendering and game logic, set that I just have these two threads.
'rip-off' said:
reply
Do you have any tips for the synchronisation issues you mentioned? Take for example synchronizing rendering and game logic, set that I just have these two threads.
When you find the word "synchronisation" in the context of multi-threading, you should be suspicious of it because what it really means is anti-threading. You should do your best to make the data that each thread is working on as independent as possible from other threads. Or rather, you should do your best to eliminate write-sharing, as that's what really kills performance.
It's alright to use critical sections/monitors/mutexes and other heavy weight blocking synchronization primitives on cold-paths, but in hot-paths that are being executed a lot, you may want to use different techniques that offer weaker synchronization guarantees. But you should usually start out using locks, make sure your code works, and then refactor hot areas of your code to use non-blocking algorithms, at least until you get some experience in concurrency programming.
Non-blocking message passing queues/stacks are ideal for notifying other threads of particular events in an independent manner. Multi-version concurrency control, read-copy-update, and software transactional memory mechanisms are often better than reader-writer locks for data where you want shared reading across threads, while synchronizing the writes. Use asynchronous file and network I/O for reading in data and processing it in stages. Design intelligent thread pool schedulers to balance and distribute your work load. Try to use non-blocking thread-local memory allocators for data internal to a particular thread, and use a scalable thread-safe memory allocator for your general purpose allocations.
Do you have any tips for the synchronisation issues you mentioned? Take for example synchronizing rendering and game logic, set that I just have these two threads.
Multi-threaded synchronization is a huge topic.
The issue boils down to the fact that, without special primitives ( Mutex, Semaphore, CAS ), you can't safely preform write operations on any variable that other threads could be reading/writing to. The end result is you have to protect all shared data. That protection is slow, so you also have to minimize the interactions performed on shared data.
For your example, your game logic will affect a large variety of rendering states. That includes stuff like: object's position matrix, object color(to make it flash red on damage), active geometry (ie turning off an arm when it gets shot off), active textures (ie much of WoW's armor and cloths are just texture swaps), etc.. All of that data has to be protected, and as Rip-off was getting at, you could make things simple with double buffering, where the Logic and Render threads read from one buffer while the logic thread write to another buffer. You eventually swap the buffers (or usually just pointers to the buffers), and everything keeps going. The difficult part is working out the swap part, as it has to be thread safe, and fair (one common problem you run into is threads stalling each other in random lurches due to lock contention that you didn't foresee ).
The biggest thing to keep in mind when you approach threading is what you are trying to accomplish. Are you looking for a crazy performance gain because you want to push the hardware to the limits or are you looking for lower key things like better game responsiveness and smoother frame rates (instead of faster frame rates)? If your goals are on the lower key end then don't over-engineer and don't over worry. Yes things like mutexes can take away from perf and cause potential locking issues but in my experience if you handle things correctly the hit rates of these kind of things can be kept to a minimum such that they aren't a concern.
You can take it even further and possibly ignore some data sharing problems if your game allows. I had a casual card game that had separate threads for rendering and animation updates. They both shared the same scene graph at the same so the animation manager could be updating an object in the middle of a rendering frame. This would obviously seem like a bad idea in theory but in practice there wasn't any downside. The worst case would be that the renderer grabbed the X and Y co-ords of a given card after the X co-ord had been updated but the Y co-ord hadn't. That would mean that the card may be off a given trajectory by a pixel or two. But since that would be fixed up in the next frame it wasn't a big deal. In fact I never noticed any such artifacts on the screen even though I'm sure it had to have happened all the time. Obviously you shouldn't do this kind of ignoring willy nilly if such data issues would be detrimental to your game but don't think that you have to achieve completely perfect data protection just for the sake of knowing that it's perfect.
You can take it even further and possibly ignore some data sharing problems if your game allows. I had a casual card game that had separate threads for rendering and animation updates. They both shared the same scene graph at the same so the animation manager could be updating an object in the middle of a rendering frame. This would obviously seem like a bad idea in theory but in practice there wasn't any downside. The worst case would be that the renderer grabbed the X and Y co-ords of a given card after the X co-ord had been updated but the Y co-ord hadn't. That would mean that the card may be off a given trajectory by a pixel or two. But since that would be fixed up in the next frame it wasn't a big deal. In fact I never noticed any such artifacts on the screen even though I'm sure it had to have happened all the time. Obviously you shouldn't do this kind of ignoring willy nilly if such data issues would be detrimental to your game but don't think that you have to achieve completely perfect data protection just for the sake of knowing that it's perfect.
That is a single thread game-loop I have used before ( I think almost everybody have used ).
1 Check for Collisions
2 Get user input
3 Update
4 Draw.
I think a simple ( and safe) way to make it multi thread is doing the following:
1 Get User input
2 update
3 Draw and check for collisions on a second thread
So , Steps 1 and 4 are done in parallel.
It is possible that one of two steps should wait for each other to complete.
I think it is a simple way that delivers a lot of performance because steps 1 and 4 are the biggest cicle eaters.
and it is safe because entity data are already updated while they are processed by two subsystems ( graphics and physics ) separately.
Of course, if you have network, it is going to run in another thread.
1 Check for Collisions
2 Get user input
3 Update
4 Draw.
I think a simple ( and safe) way to make it multi thread is doing the following:
1 Get User input
2 update
3 Draw and check for collisions on a second thread
So , Steps 1 and 4 are done in parallel.
It is possible that one of two steps should wait for each other to complete.
I think it is a simple way that delivers a lot of performance because steps 1 and 4 are the biggest cicle eaters.
and it is safe because entity data are already updated while they are processed by two subsystems ( graphics and physics ) separately.
Of course, if you have network, it is going to run in another thread.
Here's the questions you need to ask yourself before you even try this.
1) Do I have performance problems with my current single-threaded design?
2) What tasks can I do separate from the main (GUI) thread*? (* on Windows, the thread that creates your window must be the one to receive events from the system. Once you get them from the system you can pass them on to wherever, though. I believe that the same is true for Mac)
3) Which of the above uses plenty of processor time but rarely/never touches the data required in the GUI thread (textures, primarily)?
1) Do I have performance problems with my current single-threaded design?
2) What tasks can I do separate from the main (GUI) thread*? (* on Windows, the thread that creates your window must be the one to receive events from the system. Once you get them from the system you can pass them on to wherever, though. I believe that the same is true for Mac)
3) Which of the above uses plenty of processor time but rarely/never touches the data required in the GUI thread (textures, primarily)?
A lot of traditional material on "multi-threading" focuses on the practice of having shared memory between threads, synchronised via mutexes/semaphores/etc...
However, this idea of "what can I put onto a thread?" is very outdated. The question is now, "how do I write game code that runs on a modern (multicore, or even NUMA) CPU?". To make a game (engine) that seamlessly scales to multi-core CPUs, you need to be writing things at a completely different level than threads.
This is important: To write multi-threaded code, you shouldn't be dealing with threads.
Threads are used at the lowest level of your multi-core framework, but the end user of that framework (i.e. the game programmer) shouldn't even have "thread" in their vocabulary. You (the low level framework author) will use threads to implement a higher level model, such as flow-based programming, of functional programming, or the actor model, or a message-passing interface, or anything where the concept of threads/mutexes isn't required.
Things like DMA transfer alignment, or number of hardware threads, or cache atomicity should all be transparent at the game programming level. The game programmer should just be able to write functions, which your system will execute safely in a multicore environment automagically.
However, this idea of "what can I put onto a thread?" is very outdated. The question is now, "how do I write game code that runs on a modern (multicore, or even NUMA) CPU?". To make a game (engine) that seamlessly scales to multi-core CPUs, you need to be writing things at a completely different level than threads.
This is important: To write multi-threaded code, you shouldn't be dealing with threads.
Threads are used at the lowest level of your multi-core framework, but the end user of that framework (i.e. the game programmer) shouldn't even have "thread" in their vocabulary. You (the low level framework author) will use threads to implement a higher level model, such as flow-based programming, of functional programming, or the actor model, or a message-passing interface, or anything where the concept of threads/mutexes isn't required.
Things like DMA transfer alignment, or number of hardware threads, or cache atomicity should all be transparent at the game programming level. The game programmer should just be able to write functions, which your system will execute safely in a multicore environment automagically.
This topic is closed to new replies.
Advertisement
Popular Topics
Advertisement