Primitive Multi-Tasking Using Switch Statement

In all things that must be done, there’s always the right way, and then there is my way.

Today at work, I suddenly realized that my embedded application has to be multi-threaded, because there are a few tasks with strange timing requirements (that’s not the topic of this post).

This is on an embedded system with no OS, so no scheduler, setjmp/longjmp, etc.

I could install a proper RTOS (eg. FreeRTOS) to get a real scheduler, and rewrite my whole program to be multi-threaded. But it’s a big program, and it has to work by the end of the week.

So reality happened – I decided to retrofit cooperative multi-tasking into the program, using what I think is a novel approach I just invented – switch statements!

This is what the code looked like. Just regular update loop.

void UpdateSubsystem1()
{
	...
}

void UpdateSubsystem2()
{
	...
}

void UpdateSubsystem3()
{
	...
}

void Update()
{
	UpdateSubsystem1();
	UpdateSubsystem2();
	UpdateSubsystem3();
}

Some of the subsystem update functions take a long time, because, for example, they send things over communication buses.

I can rewrite everything as state machines, but time is of the essence… So here is what I did to each subsystem update function.

First, I enclosed the whole body of the function in a switch statement, and inserted case labels at my “restart points” (points where I want to be able to “yield”). I also added a static variable to hold which stage the function/task is in.

So if my original code looked like this –

void UpdateSubsystem()
{
	doSomething();
	
	while (!SomethingHappened()) {}
	
	doSomething();
	
	while (!SomethingHappened()) {}
	
	doSomething();
}

Now it looks like this –

void UpdateSubsystem()
{
	static int stage = 0;

	switch(stage)
	{
	case 0:
		doSomething();

	case 1:
		while (!SomethingHappened()) {}

		doSomething();

	case 2:
		while (!SomethingHappened()) {}

		doSomething();
	}
}

Note that there are no breaks. This is intentional. We want the program to fall through everything.

Then, when I want the task to yield, I just have to set “stage” to the restart point, and simply return!

void UpdateSubsystem()
{
	static int stage = 0;

	switch(stage)
	{
	case 0:
		doSomething();

	case 1:
		if (!SomethingHappened())
		{
			stage = 1;
			return;
		}

		doSomething();

	case 2:
		if (!SomethingHappened())
		{
			stage = 2;
			return;
		}

		doSomething();

		stage = 0;
	}
}

Cool eh?

A few caveats –
1. Most importantly, all variables must be static/global. Local variables can only be used between 2 restart points, because the stack frame for the call is destroyed when the function returns.
2. Cannot start the same task more than once. Function is no longer re-entrant due to the use of static variables.

Probably a few more I missed.

This is not patented already, right?

Update:
Macro implementation:

#define RESTARTABLE_BEGIN static int restartable_stage = 0; switch(restartable_stage) { case 0:
#define RESTARTABLE_YIELD restartable_stage = __LINE__; return false; case __LINE__: 
#define RESTARTABLE_END } restartable_stage = 0; return true;

Example:

bool task1()
{
	RESTARTABLE_BEGIN;

	std::cout << "t1 -> 1" << std::endl;

	RESTARTABLE_YIELD;

	std::cout << "t1 -> 2" << std::endl;

	RESTARTABLE_YIELD;

	std::cout << "t1 -> 3" << std::endl;

	RESTARTABLE_END;
}

bool task2()
{
	RESTARTABLE_BEGIN;

	std::cout << "t2 -> 1" << std::endl;

	RESTARTABLE_YIELD;

	std::cout << "t2 -> 2" << std::endl;

	RESTARTABLE_YIELD;

	std::cout << "t2 -> 3" << std::endl;

	RESTARTABLE_YIELD;

	std::cout << "t2 -> 4" << std::endl;

	RESTARTABLE_YIELD;

	std::cout << "t2 -> 5" << std::endl;

	RESTARTABLE_END;
}

int main(int argc, char* argv[])
{
	bool task1_done = false;
	bool task2_done = false;

	while (!(task1_done && task2_done))
	{
		if (!task1_done)
		{
			task1_done = task1();
		}

		if (!task2_done)
		{
			task2_done = task2();
		}
	}

	return 0;
}

This program actually triggers a bug in VS2010. It doesn't like __LINE__ as a case label (saying it's not constant) if edit and continue debugging is turned on.

Workaround provided by putty (http://rc.quest.com/viewvc/putty/branches/group-policy/putty/ssh.c?view=markup) -

337 * In particular, if you are getting `case expression not constant'
338 * errors when building with MS Visual Studio, this is because MS's
339 * Edit and Continue debugging feature causes their compiler to
340 * violate ANSI C. To disable Edit and Continue debugging:
341 *
342 * - right-click ssh.c in the FileView
343 * - click Settings
344 * - select the C/C++ tab and the General category
345 * - under `Debug info:', select anything _other_ than `Program
346 * Database for Edit and Continue'.
347 */