The dining philosophers problem is a classic computer science textbook problem for demonstrating the use of multithreading. As Wikipedia says:
Five silent philosophers sit at a round table with bowls of spaghetti. Forks are placed between each pair of adjacent philosophers.
Each philosopher must alternately think and eat. However, a philosopher can only eat spaghetti when they have both left and right forks. Each fork can be held by only one philosopher and so a philosopher can use the fork only if it is not being used by another philosopher. After an individual philosopher finishes eating, they need to put down both forks so that the forks become available to others. A philosopher can only take the fork on their right or the one on their left as they become available and they cannot start eating before getting both forks.
Eating is not limited by the remaining amounts of spaghetti or stomach space; an infinite supply and an infinite demand are assumed.
The problem is how to design a discipline of behavior (a concurrent algorithm) such that no philosopher will starve; i.e., each can forever continue to alternate between eating and thinking, assuming that no philosopher can know when others may want to eat or think.
The problem was designed to illustrate the challenges of avoiding deadlock, a system state in which no progress is possible.
In summary, then, this is a classical problem in multithreading, demonstrating the need to avoid resource starvation using mutual exclusion principles.
I want to implement such a program in real-mode DOS, but DOS clearly lacks multithreading capabilities.
I am aware of third-party software such as RTKernel, but this seems like overkill for this situation.
Is there any solution to simulate multithreading so that I can program a simulation of the dining philosophers problem in DOS, using 16-bit x86 assembly language?
Multithreading is about creating the illusion that multiple execution paths in a program run simultaneously. On today's multi-core computers this doesn't have to be an illusion anymore if the number of threads stays within limits.
A different road to multithreading
In the preemptive multitasking model the running out of a timeslice triggers a thread switch. The switch is initiated from outside the running thread.
In the multithreading module that I have written, the switch can not happen without the running thread's approval and collaboration. It is the running thread that decides where, but not when, a switch can take place. To this end the programmer has to insert
calls to a function MaybeYieldThread at strategically chosen places in the thread. Loops are good places for this. If at the moment of such a call, the timeslice has not yet elapsed then the call will instantly return. If the timeslice has elapsed then the MaybeYieldThread momentarily acts like a true YieldThread and the switch happens.The major advantage of this approach is that it can avoid many of the race conditions for which you would normally be using synchronization objects like mutexes, semaphores, or critical sections. You insert your
call MaybeYieldThreadinstruction(s) where it is thread-safe and that's it!The main characteristics
The multithreading capabilities are encoded in a single source file mtModule.INC that you include in your application anywhere you like.
The api description
The api that I propose is a small one, but I believe it delivers all the multithreading capability a DOS program could need... At some moment I had implemented features like thread handles, thread priorities, and inter thread communication. In retrospect and bearing in mind the saying "Less is More", I am glad that I removed all of these.
It all starts with a
callto BeginSessionThread. You define the borders of the session memory where all of the thread's stacks will be placed, you define the timeslice to be used, and you point to the first thread that immediately receives control if no errors were encountered.Among the things that the first thread will do is creating additional threads using CreateThread. What you provide is the code address of the other thread and the amount of memory you wish to use for its stack.
Once threads are up and running, they can use YieldThread to give up control in favor of the next thread, use MaybeYieldThread to give up control if, and only if, the timeslice that they are running in has elapsed, and use SleepThread to give up control and remove themselves from being scheduled until the requested duration is over.
If a thread has outlived its purpose, a
call(orjmp) to ExitThread or a mereretinstruction (from a balanced stack of course!) removes the thread permanently from the scheduler and returns the memory that its stack occupied, to the pool of free session memory.When no more multithreading is needed, a
call(orjmp) to EndSessionThread will return control to the instruction directly below from where the session was started (thecall BeginSessionThreadinstruction). It is possible to pass an exitcode.Alternatively, exiting from the last active thread will also end the session, but in this case the exitcode will be zero.
In order to suspend the multithreading session, you can call StopSessionThread. It will reset the timer frequency to the standard 18.2 Hz and freeze all pending SleepTimes. To resume the multithreading session, all it takes is a
callto ContSessionThread. Suspending the session is one way to temporarily pause the program without disturbing the SleepTimes. And if you want to EXEC a child program or even launch a nested multithreading session, suspending the current session is mandatory for success.The api quick reference
Some points of interest
It is mandatory that a thread doesn't change the
SSsegment register and that it leaves about 80 bytes on the stack for use by the mtModule.INC.For optimal 'preemptiveness', you should not use MaybeYieldThread too sparsely. On the other hand for efficiency reasons, you should perhaps not be using MaybeYieldThread in a tight loop.
An example application
Next demo program uses every function available in the above api. Its sole purpose is to demonstrate how to use the api functions, nothing more.
It's easy to experiment with different timeslices because you can specify the length of the timeslice (expressed in milliseconds) on the commandline.
The program runs fine in true real address mode and under emulation (Windows CMD and DOSBox).