To get a feel for what was happening with protothreads, a while back I hacked together an implementation using C++ and (of course) some macro trickery. It was a good exercise, and I'd recommend it for anyone considering going the protothread route. For me, protothreads are cute, but are a
leaky abstraction that probably causes more harm than good.
One of the drawbacks of the protothread idea is that you can't rely on local variables in loops. Every time the program executes something that would have been a blocking call in a real RTOS, the protothread implementation does saves some magic state and then returns from the function. For example:
PTK_BEGIN();
int i;
for(i=0; i < 10; ++i) {
do_something();
PTK_SLEEP(100);
}
PTK_END();
That loop index isn't going to work the way you want because PTK_SLEEP() actually causes the entire function to return, losing the contents of every local variable. I thought C++ would be a nice way to get around this because by having threads be instances of a class, I could write the same kind of code, but use member variables instead of locals. That part worked out well.
However, there's some serious macro magic going on under the covers. If you have to step into those expansions during a debugging session, you're in for some turbulence.
On the plus side, protothreads don't create as much memory pressure as "real threads" because there's only one stack to allocate. Another interesting feature is that it's pretty easy to simulate a protothread kernel under another operating system (e.g., Unix), so you can test out your code without having to run it on an embedded system.
The biggest drawback is having to re-write your code so that it doesn't use local variables that need to persist across scheduler operations (and convincing your co-workers to do the same). Debugging protothread code can be pretty painful too. IMO, it's far worse than stepping over an RTOS call because most modern debuggers can handle stepping over a function call (even if it results in a context switch) easily. They don't handle the macro trickery that lives underneath the macro facade of protothreads.
#define PTK_LABEL_AT_LINE_HELPER(n) PTK_LINE_##n
#define PTK_LABEL_AT_LINE(n) PTK_LABEL_AT_LINE_HELPER(n)
#define PTK_HERE PTK_LABEL_AT_LINE(__LINE__)
#if defined(PTK_DEBUG)
#define PTK_DEBUG_SAVE() \
file = __FILE__; \
line = __LINE__;
#else
#define PTK_DEBUG_SAVE()
#endif
#define PTK_BEGIN() \
do { \
if (continuation != 0) goto *continuation; \
this->timer_expiration = TIME_NEVER; \
} while (0)
#define PTK_YIELD() \
do { \
state = YIELDED_STATE; \
continuation = &&PTK_HERE; \
PTK_DEBUG_SAVE(); \
return; \
PTK_HERE: ; \
} while (0)
#define PTK_SLEEP(duration) \
do { \
kernel.lock(); \
state = SLEEPING_STATE; \
kernel.unschedule(*this); \
kernel.arm_timer(*this, (duration)); \
continuation = &&PTK_HERE; \
PTK_DEBUG_SAVE(); \
kernel.unlock(); \
return; \
PTK_HERE: ; \
} while (0)
#define PTK_END() \
do { \
thread_exit: \
ptk_end(); \
void *pexit = &&thread_exit; \
(void) pexit; \
} while (0)
I'm with nctnico on this one.