multithreadinghardware

Is data synchronization necessary on single-threaded systems?


This question stems from a technical interview where I was asking candidate about multithreading, and the differences between "true multithreading" via multiple hardware cores and/or hyperthreading and "apparent multithreading" aka non-cooperative multitasking via OS thread switching.

We then started to wonder whether on a system with only the latter (single physical thread), is it still necessary to use data synchronization primitives like atomics, mutexes and barriers when using multiple logical threads e.g. for non-blocking IO. Neither of us really knew, so we left it as theoretical trivia to clarify later.

Arguments for no:

Arguments for yes:

So which is it? Does it depend on architecture, e.g. x64 vs ARM vs RISCV? On OS? Is it worth wrapping all synchronization in #ifndef NO_MULTITHREADING, or would such hardware have all relevant instructions nooped?


Solution

  • Yes, synchronization is necessary in any environment that enables pre-emptive multitasking, even if there is only a single processor core.

    The reason for this is that the activation of the OS's context-switch timer is indeterminate with respect to the execution of instructions inside the various executing threads -- a given thread has no way to know or predict precisely when it will get kicked off of the CPU and put to sleep so that another thread can run in its place. Therefore it still needs to wrap any access to shared resources in a mutex (or similar) to avoid race conditions caused e.g. by another thread taking over the CPU and writing to the shared resource while it was still in the middle of reading from it.

    Some OS's (such as the original versions of MacOS and Windows) avoided this requirement by implementing only co-operative multithreading instead of pre-emptive multithreading -- in co-operative multithreading, a thread is allowed to continue executing for as long as it likes, and must explicitly call a function (like WaitNextEvent()) in order to allow other threads to execute. However, that approach has its own problems, because any poorly-crafted program can take over the CPU and rarely or never give it back, effectively starving all other threads for CPU time and leading to a poor user experience.

    Is it worth wrapping all synchronization in #ifndef NO_MULTITHREADING,

    The NO_MULTITHREADING preprocessor-token would only be useful if your program is sometimes compiled to run in environments where it is guaranteed that only a single thread will be ever be accessing your APIs at any given time. Since that guarantee is probably quite unusual, it's probably not worth implementing the conditional logic... but if you do, you can implement it inside your Mutex API (effectively turning your lock() and unlock() calls into no-ops when NO_MULTITHREADING is defined) so that the calling code doesn't have to get littered with #ifdefs everywhere.