Well, we could do coöperative (nonpreëmptive) multi-tasking by creating state machines to handle each loop:
private interface IStateMachine
{
void DoNext();
}
private class Loop0 : IStateMachine
{
private int _state;
private int x;
public void DoNext()
{
switch (_state)
{
case 0:
x++;
_state = 1;
break;
case 1:
x++; // This is of course the same as previous, but I'm matching
// the code in your question. There's no reason why it need
// not be something else.
_state = 2;
break;
case 2:
x++;
_state = 3;
break;
case 3:
x++;
_state = 0;
break;
}
}
}
private class Loop1 : IStateMachine
{
private int _state;
private int x;
public void DoNext()
{
switch (_state)
{
case 0:
Console.WriteLine("X=" + x);
_state = 1;
break;
case 1:
Console.WriteLine("X=" + x);
_state = 2;
break;
case 2:
Console.WriteLine("X=" + x);
_state = 3;
break;
case 3:
Console.WriteLine("X=" + x);
_state = 0;
break;
}
}
}
private static void Driver()
{
// We could have all manner of mechanisms for deciding which to call, e.g. keep calling one and
// then the other, and so on. I'm going to do a simple time-based one here:
var stateMachines = new IStateMachine[] { new Loop0(), new Loop1() };
for (int i = 0;; i = (i + 1) % stateMachines.Length)
{
var cur = stateMachines [i];
DateTime until = DateTime.UtcNow.AddMilliseconds (100);
do
{
cur.DoNext ();
} while (DateTime.UtcNow < until);
}
}
There are two big problems with this:
- The
x
in each is a separate x
. We need to box the int
or wrap it in a reference type so that both methods can be accessing the same variable.
- The relationship between your loops and these state machines isn't very clear.
Luckily there already exists a way (indeed more than one) to write a method in C# that is turned into a state-machine with a method for moving to the next state that handles both of these issues:
private static int x;
private static IEnumerator Loop0()
{
for(;;)
{
x++;
yield return null;
x++;
yield return null;
x++;
yield return null;
x++;
yield return null;
}
}
private static IEnumerator Loop1()
{
for(;;)
{
Console.WriteLine("X=" + x);
yield return null;
Console.WriteLine("X=" + x);
yield return null;
Console.WriteLine("X=" + x);
yield return null;
Console.WriteLine("X=" + x);
yield return null;
}
}
private static void Driver()
{
// Again, I'm going to do a simple time-based mechanism here:
var stateMachines = new IEnumerator[] { Loop0(), Loop1() };
for (int i = 0;; i = (i + 1) % stateMachines.Length)
{
var cur = stateMachines [i];
DateTime until = DateTime.UtcNow.AddMilliseconds (100);
do
{
cur.MoveNext ();
} while (DateTime.UtcNow < until);
}
}
Now not only is it easy to see how this relates to your loops (each of the two methods have the same loop, just with added yield return
statements), but the sharing of x
is handled for us too, so this example actually shows it increasing, rather than an unseen x
incrementing and a different x
that is always 0
being displayed.
We can also use the value yield
ed to provide information about what our coöperative "thread" wants to do. For example, returning true
to always give up its time slice (equivalent to calling Thread.Yield()
in C# multi-threaded code):
private static int x;
private static IEnumerator<bool> Loop0()
{
for(;;)
{
x++;
yield return false;
x++;
yield return false;
x++;
yield return false;
x++;
yield return true;
}
}
private static IEnumerator<bool> Loop1()
{
for(;;)
{
Console.WriteLine("X=" + x);
yield return false;
Console.WriteLine("X=" + x);
yield return false;
Console.WriteLine("X=" + x);
yield return false;
Console.WriteLine("X=" + x);
yield return true;
}
}
private static void Driver()
{
// The same simple time-based one mechanism, but this time each coroutine can
// request that the rest of its time-slot be abandoned.
var stateMachines = new IEnumerator<bool>[] { Loop0(), Loop1() };
for (int i = 0;; i = (i + 1) % stateMachines.Length)
{
var cur = stateMachines [i];
DateTime until = DateTime.UtcNow.AddMilliseconds (100);
do
{
cur.MoveNext ();
} while (!cur.Current && DateTime.UtcNow < until);
}
}
As I'm using a bool
here I have only two states that affect how Driver()
(my simple scheduler) acts. Obviously a richer datatype would allow for more options, but be more complex.
One possibility would be to have your compiler have a type of method that must return void
(comparable to how yield
and await
have restrictions on the return types of methods that use them in C#) which could contain keywords like thread opportunity
, thread yield
and thread leave
which would then be mapped to yield return false
, yield return true
and yield break
in the C# above.
Of course, being coöperative it requires explicit code to say when other "threads" might have an opportunity to run, which in this case is done by the yield return
. For the sort of preëmptive multi-threading that we enjoy just writing in C# for the operating systems it can run on, where time slices can end at any point rather than just where we explicitly allow it will require you to compile the source to produce such state machines, without their being instructions in that source. This is still coöperative, but forces that coöperation out of the code when compiling.
Truly preëmptive multi-threading would require that you have some way of storing the current state of each loop when switching to another thread (just as the stack of each thread in a .NET program does). In a virtual OS you could do this by building threads on top of the underlying OS's threads. In a non-virtual OS you're likely going to have to build your threading mechanism closer to the metal, with the scheduler changing the instruction pointer when threads change,