14
\$\begingroup\$

I'm creating a simple 2D game engine and I want to update and render the sprites in different threads, to learn how it is done.

I need to synchronise the update thread and the render one. Currently, I use two atomic flags. The workflow looks something like:

Thread 1 -------------------------- Thread 2
Update obj ------------------------ wait for swap
Create queue ---------------------- render the queue
Wait for render ------------------- notify render done
Swap render queues ---------------- notify swap done

In this setup I limit the render thread's FPS to the update thread's FPS. Besides I use sleep() to limit both render and update thread's FPS to 60, so the two wait functions won't wait much time.

The problem is:

The average CPU usage is around 0.1%. Sometimes it's go up to 25% (in a quad core PC). It means that a thread is waiting for the other because the wait function is a while loop with a test and set function, and a while loop will use all your CPU resources.

My first question is: is there another way to synchronise the two threads? I noticed that std::mutex::lock don't use the CPU while it is waiting to lock a resource so it isn't a while loop. How does it work? I can't use std::mutex because I will need to lock them in one thread and unlock in another thread.

The other question is; since the program runs always at 60 FPS why does sometimes its CPU usage jumps to 25%, meaning that one of the two wait is waiting a lot? (the two threads are both limited to 60fps so they ideally won't need a lot of synchronisation).

Edit: Thanks for all the replies. First I want to say I don't start a new thread each frame for render. I start both update and render loop at the beginning. I think multithreading can save some time : I have the following functions: FastAlg() and Alg(). Alg() is both my Update obj and render obj and Fastalg() is my "send render queue to" renderer" ". In a single thread :

Alg() //update 
FastAgl() 
Alg() //render

In two thread:

Alg() //update  while Alg() //render last frame
FastAlg() 

So maybe multithreading can save same time. (actually in a simple math application it does, where alg is a long algorithm amd fastalg a faster one)

I know that sleep isn't a good idea, although I ve never have problems. Will this will better?

While(true) 
{
   If(timer.gettimefromlastcall() >= 1/fps)
   Do_update()
}

But this will be a infinite while loop that will use all the CPU. Can I use sleep(a number < 15) to limit the usage? In this way it will run at, for example, 100 fps, and the update function will be called just 60 times per second.

To synchronise the two threads I will use waitforsingleobject with createSemaphore so I will able to lock and unlock in different thread (whitout using a while loop), won't I?

\$\endgroup\$
11
  • 5
    \$\begingroup\$ "Don't tell my multithreading is useless in this case, I just want to learn how to do it" -- in that case you should learn things properly, that is (a) do not use sleep() to control the frame rare, never ever, and (b) avoid thread-per-component design and avoid running lockstep, instead split work in tasks and handle tasks from a work queue. \$\endgroup\$
    – Damon
    Commented Jul 10, 2014 at 14:03
  • 2
    \$\begingroup\$ @Damon (a) sleep() can be used as a frame rate mechanism and is in fact quite popular, though I have to agree that there is far better options. (b) The user here wants to separate both update and render in two different threads. This is a normal separation in a game engine and is not so "thread-per-component". It gives clear advantages but it can bring problems if done incorrectly. \$\endgroup\$ Commented Jul 10, 2014 at 22:18
  • \$\begingroup\$ @AlphSpirit: The fact that something is "common" does not mean that it isn't wrong. Without even going into divergent timers, the mere granularity of sleep on at least one popular desktop operating system is reason enough, if not its unreliability-per-design on every existing consumer system. Explaining why separating update and render into two threads as described is unwise and causes more trouble than it's worth would take too long. The OP's goal is stated as learn how it's done, which should be learn how it's done correctly. Plenty of articles on modern MT-engine design around. \$\endgroup\$
    – Damon
    Commented Jul 11, 2014 at 10:30
  • \$\begingroup\$ @Damon When I said it was popular, or common, I did not meant to say it was right. I just meant it was used by many people. "...though I have to agree that there is far better options" meant that it is indeed not a very good way to synchronize time. Sorry for the misunderstanding. \$\endgroup\$ Commented Jul 11, 2014 at 11:32
  • 1
    \$\begingroup\$ @AlphSpirit Which is the right way? \$\endgroup\$
    – Liuka
    Commented Jul 11, 2014 at 15:54

2 Answers 2

29
\$\begingroup\$

For a simple 2D engine with sprites, a single-threaded approach is perfectly good. But since you want to learn how to do multithreading, you should learn to do it correctly.

Do not

  • Use 2 threads that run more or less lock-step, implementing a single-threaded behavior with several threads. This has the same level of parallelism (zero) but adds overhead for context switches and synchronization. Plus, the logic is harder to grok.
  • Use sleep to control the frame rate. Never. If someone tells you to, hit them.
    First, not all monitors run at 60Hz. Second, two timers ticking at the same rate running side by side will always eventually get out of sync (drop two pingpong balls on a table from the same height, and listen). Third, sleep is by design neither accurate nor reliable. Granularity may be as bad as 15.6ms (in fact, the default on Windows[1]), and a frame is only 16.6ms at 60fps, which leaves a mere 1ms for everything else. Plus, it's hard to get 16.6 to be a multiple of 15.6...
    Also, sleep is allowed to (and will sometimes!) return only after 30 or 50 or 100 ms, or an even longer time.
  • Use std::mutex to notify another thread. This is not what it's for.
  • Assume that TaskManager is any good at telling you what's going on, especially judging from a number like "25% CPU", which could be spent in your code, or within the usermode driver, or somewhere else.
  • Have one thread per high level component (there are of course some exceptions).
  • Create threads at "random times", ad hoc, per task. Creating threads can be surprisingly expensive and they can take a surprisingly long time before they are acutally doing what you told them (especially if you have a lot of DLLs loaded!).

Do

  • Use multithreading to have things run asynchronously as much as you can. Speed is not the main idea of threading, but doing things in parallel (so even if they take longer alltogether, the sum of all is still less).
  • Use vertical sync to limit the frame rate. That is the only correct (and non-failing) way to do it. If the user overrides you in the display driver's control panel ("force off"), then so be it. After all it's his computer, not yours.
  • If you need to "tick" something at regular intervals, use a timer. Timers have the advantage of having a much better accuracy and reliability as compared to sleep[2]. Also, a recurring timer accounts for time correctly (including time that passes in between) whereas sleeping for 16.6ms (or 16.6ms minus measured_time_elapsed) doesn't.
  • Run physics simulations that involve numeric integration at a fixed time step (or your equations will explode!), interpolate graphics between steps (this may be an excuse for a separate per-component-thread, but it can also be done without).
  • Use std::mutex to have only one thread access a resource at a time ("mutually exclude"), and to comply with the weird semantics of std::condition_variable.
  • Avoid having threads compete for resources. Lock as little as necessary (but none less!) and hold locks only as long as absolutely necessary.
  • Do share read-only data between threads (no cache issues, and no locking necessary), but do not concurrently modify data (needs sync and kills the cache). That includes modifying data that is nearby a location someone else might read.
  • Use std::condition_variable to block another thread until some condition is true. The semantics of std::condition_variable with that extra mutex are admittedly pretty weird and twisted (mostly for historic reasons inherited from POSIX threads), but a condition variable is the correct primitive to use for what you want.
    In case you find std::condition_variable too weird to be comfortable with it, you can as well simply use a Windows event (slightly slower) instead or, if you are courageous, build your own simple event around NtKeyedEvents (involves scary low level stuff). As you use DirectX, you're already bound to Windows anyway, so loss of portability shouldn't a biggie.
  • Break work into reasonably-sized tasks that are run by a fixed-size worker thread pool (no more than one per core, not counting hyperthreaded cores). Let finishing tasks enqueue dependent tasks (free, automatic synchronization). Make tasks that have at least a few hundred non-trivial operations each (or one lenghty blocking operation like a disk read). Prefer cache-contiguous access.
  • Create all threads at program start.
  • Take advantage of asynchronous functions that the OS or the graphics API offers for better/additional parallelism, not only on the program level but also on the hardware (think PCIe transfers, CPU-GPU parallelism, disk DMA, etc.).
  • 10,000 other things that I've forgotten to mention.


[1] Yes, you can set the scheduler's rate down to 1ms, but this is frowned upon as it causes a lot more context switches, and consumes a lot more power (in a world where more and more devices are mobile devices). It also isn't a solution since it still does not make sleep any more reliable.
[2] A timer will boost the thread's priority, which will allow it to interrupt another equal-priority thread mid-quantum and be scheduled first, which is a quasi-RT behavior. It is of course not true RT, but it comes very close. Waking from sleep merely means that the thread becomes ready to be scheduled at some time, whenever that may be.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Can you please explain why you should not "Have one thread per high level component"? Do you mean that one should not have physics and audio mixing in two separate threads? I don't see any reason to not do so. \$\endgroup\$ Commented Jul 5, 2017 at 12:26
  • 1
    \$\begingroup\$ @ElvissStrazdins I assume so it doesn't conflict with the "don't spawn more threads than physical cores" suggestion. You could fairly easily have a system that automatically scales updates across the CPU dependant on the number of physical cores available. \$\endgroup\$
    – AStopher
    Commented Oct 7, 2020 at 11:31
  • \$\begingroup\$ @AStopher thanks! That makes sense. \$\endgroup\$ Commented Oct 8, 2020 at 13:27
3
\$\begingroup\$

I am not sure what you want to achieve by limiting the FPS of the Update and Render both to 60. If you limit them to the same value, you could have just put them in the same thread.

The goal when separating Update and Render in different threads is to have both "almost" independent of one another, so that the GPU can render 500 FPS and the Update logic still goes at 60 FPS. You do not achieve a very high performance gain by doing so.

But you said you just wanted to know how it works, and it's fine. In C++, a mutex is a special object that is used to lock access to certain resources for other threads. In other words, you use a mutex to make sensible data accessible by only one thread at the time. To do so, it is quite simple:

std::mutex mutex;
mutex.lock();
// Do sensible stuff here...
mutex.unlock();

Source: http://en.cppreference.com/w/cpp/thread/mutex

EDIT: Make sure your mutex is class or file-wide, as in the link given, or else each thread will create its own mutex and you will not achieve anything.

The first thread to lock the mutex will have access to the code inside. If a second thread tries to call the lock() function, it will block until the first thread unlocks it. So a mutex is a blocing function, unlike a while loop. Blocking functions will not put stress on the CPU.

\$\endgroup\$
4
  • \$\begingroup\$ And how does the block work? \$\endgroup\$
    – Liuka
    Commented Jul 10, 2014 at 13:36
  • \$\begingroup\$ When the second thread will call lock(), it will patiently wait for the first thread to unlock the mutex, and will continue on the next line after (in this example, the sensible stuff). EDIT: The second thread will then lock the mutex for itself. \$\endgroup\$ Commented Jul 10, 2014 at 13:39
  • 1
    \$\begingroup\$ linuxquestions.org/questions/programming-9/… \$\endgroup\$ Commented Jul 10, 2014 at 13:40
  • 1
    \$\begingroup\$ Use std::lock_guard or similar, not .lock()/.unlock(). RAII is not just for memory management! \$\endgroup\$
    – bcrist
    Commented Jul 11, 2014 at 22:04

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .