On 09/25/2016 06:39 AM, Jacob Carlborg wrote:
I find it quite difficult to find correct definitions with many of these
terms like: coroutine, fiber, stackless thread, resumeable function.
Because many languages use the same name of different things and they
use different names for the same things.
FWIW, and this is at least the way I see it:
(BTW, any domain experts please chime in if I'm wrong. I'm thinking it
may be worth reposting this as a blog entry, as long as I'm not too far
off-base.)
Fiber:
------
Like a thread, but lighter-weight. Multiple fibers can run within one
thread.
Like a thread, a fiber has it's own stack, and upon suspend/resume
various state is saved/restored (I assume that "state" means "CPU
registers").
Unlike a thread, a fiber's multitasking is sequential and cooperative,
NOT simultaneous and preemptive: Ie, One fiber must willfully relinquish
control (by calling a "yield" function) before another fiber in the same
thread can resume. I'm assuming this sequential/cooperative what allows
it to be lighter-weight than a true thread, but I'm not 100% certain.
Coroutine:
----------
A function that can "yield" (ie, it can choose to temporarily exit so
that it can be resumed from the same place later on). Often a return
value is emitted upon yielding, so these are usually used as generators,
because they're a convenient way to generate a series of values in an
easy procedural style.
Coroutines are commonly implemented as fibers, so it is often said that
fibers and coroutines are the same thing. That's partially accurate,
except for two things:
1. "Coroutine" refers to a specific function, whereas "fiber" refers to
the overall sequence of execution.
2. This is probably a debatable matter of semantics, but coroutines
don't have to be implemented using fibers. For example, opApply is
essentially a coroutine (it yields by calling the delegate that was
passed in).
Stackless Thread/Fiber:
-----------------------
Not a true thread or fiber. It's called a "stackless thread" or
"stackless fiber" simply because it's *like* a thread or fiber (closer
to a fiber, really), but without a separate stack for each,
umm..."fiber". Protothreads is an example of a stackless thread/fiber.
A stackless thread/fiber is much more lightweight than even a fiber. But
it comes with a limitation:
With a true fiber, any function in the callstack can yield. When it
yields, it suspends the fiber's entire callstack. So inside a fiber,
your coroutine can call a function and have *that* function yield your
fiber. That feature requires a separate callstack for each fiber, so you
cannot do that with the stackless versions.
Instead, in a stackless thread/fiber, yielding will ONLY suspend the
current function, not a full callstack (since it doesnt have a callstack
of its own to suspend).
In other words, yielding a stackless thread/fiber works much like a
normal "return" statement:
-------------------
void foo() {
bar();
}
void bar() {
return; // ONLY return from bar
return from BOTH bar AND foo; // IMPOSSIBLE!!!
}
-------------------
A stackless thread/fiber works the same (psuedocode):
-------------------
void coroutineFoo() {
coroutineBar();
}
void coroutineBar() {
// Stackless fiber: ONLY suspends coroutineBar.
// Regular fiber: Suspends ENTIRE callstack.
yield;
yield from BOTH bar AND foo;// IMPOSSIBLE with stackless
// More code here....
}
-------------------
Luckily, you can work around it, just like you can with normal returning
(psuedocode):
-------------------
void coroutineFoo() {
bool shouldYield = coroutineBar();
if(shouldYield)
yield;
// More code here....
}
// Yielded value: Should the caller yield?
bool coroutineBar() {
yield true; // Yield both bar and foo
// More code here....
}
-------------------
I'd argue opApply's approach technically qualifies as a stackless
thread/fiber, since it yields (by calling the delegate it received) and
does not involve any extra callstack. Plus, like any other stackless
thread/fiber, it cannot suspend any further up the callstack than just
itself.
However, from what I've seen, a stackless thread/fiber is typically
implemented the way Protothreads works:
When the "stackless coroutine" function is called, the first thing done
inside the function is jump to wherever the function last left off.
Then, every "yield" is constructed as an *actual* "return" followed by a
jump label. When a function yields, it returns three things:
1. An optional value being yielded (like a normal return value).
2. Which jump label to jump to next time the function is resumed.
3. Any local variables that need to be preserved upon resume (or just
all local variables).
With a really nice stackless thread/fiber system, like C#'s, this is all
handled behind-the-scenes.
Stackless Coroutine:
--------------------
A coroutine that uses a stackless thread/fiber instead of a normal fiber.
Resumable Function:
-------------------
This sounds like a vague general term to me. I would think it casually
refers to any function that can be suspended/resumed: A Fiber-based
coroutine, a stackless coroutine, or whatever else may or may not exist.
Ah, right. Previously when I looked at this I only saw examples which
stored the jump label. That might be possible to automatically rewrite
in a AST macro.
Would it be possible to store the state in a TLS variable inside the
function to avoid having to pass in the state?
I don't see why not, but there would be one notable drawback: You could
only have one instance existing at a time (per thread).
Ex: If you pass the state through params and return values instead of a
static function variable, you can do this:
----------------------
// A manual stackless coroutine in D
// Would be much simpler with built-in support
struct YieldInfo {
State state;
int yieldedValue;
}
struct State {
int jumpLabel = 1;
// Any additional locals here
}
// Generate three multiples of 'multiplier', repeating forever
YieldInfo generateMultiples(
State state, int multiplier
) {
switch(state.jumpLabel) {
case 1:
// Yield first multiple
return YieldInfo(State(2), 1 * multiplier);
case 2:
// Yield second multiple
return YieldInfo(State(3), 2 * multiplier);
case 3:
// Yield third multiple
return YieldInfo(State(1), 3 * multiplier);
}
}
void main()
{
// One instance of generateMultiples
YieldInfo a;
a = generateMultiples(a, 2); assert(a.yieldedValue == 2);
a = generateMultiples(a, 2); assert(a.yieldedValue == 4);
a = generateMultiples(a, 2); assert(a.yieldedValue == 6);
a = generateMultiples(a, 2); assert(a.yieldedValue == 2);
// Do another instance at the same time:
YieldInfo b;
b = generateMultiples(b, 5); assert(b.yieldedValue == 5);
a = generateMultiples(a, 2); assert(a.yieldedValue == 4);
b = generateMultiples(b, 5); assert(b.yieldedValue == 10);
a = generateMultiples(a, 2); assert(a.yieldedValue == 6);
b = generateMultiples(b, 5); assert(b.yieldedValue == 15);
}
----------------------
Actual stackless coroutine support would make that much simpler of course:
----------------------
CoroutineInputRange!int generateMultiples(int multiple) {
while(true) {
foreach(i; 1...4)
yield i * multiple;
}
}
void main()
{
auto a = generateMultiples(2);
assert(a.front == 2);
a.popfront(); assert(a.front == 4);
a.popfront(); assert(a.front == 6);
a.popfront(); assert(a.front == 2);
auto b = generateMultiples(5);
assert(b.front == 5);
a.popfront(); assert(a.front == 4);
b.popfront(); assert(b.front == 10);
a.popfront(); assert(a.front == 6);
b.popfront(); assert(b.front == 15);
}
----------------------
Naturally, if the state was stored as the function's static locals, you
woudn't be able to do that. Instance "b" would clobber instance "a".
2. Compared to C, D has much stricter rules about where a switch's
"case" labels can do. Ie, they can't really cross scope boundaries. So
*all* the coroutine's "yield" statements would have to be within the
same scope *and* at the same nesting-level (or at the very least,
there'd be some big annoying restrictions). Otherwise, when it gets
converted to a "case:", the resulting code would be invalid.
Do you have any examples?
I'd have to look it up and refresh my memory. I just remember D has more
restrictions regarding jumping to labels than C has. Something like:
------------------
State myCoroutine(State s) {
yield "hello";
if(foo) {
yield " world";
}
yield "!";
}
------------------
A Protothreads-like preprocessing approach that used switch/case would
turn that into:
------------------
State myCoroutine(State s) {
switch(s.jumpLabel){
case 0:
// Start converted code: //////////////
//yield "hello"; -> return+case:
return State(1, "hello");
case 1:
if(int foo = getFoo()) {
//yield " world"; ->
return State(2, " world"); return+case:
case 2:
}
//yield "!"; -> return+case:
return State(3, "!");
case 3:
// End converted code //////////////
break;
}
}
------------------
That "if" really messed things up. AIUI, I don't think D will allow
things like that.
C doesn't have so much of a problem with any of this, it famously just
permits pretty much anything, which is why Protothreads works there.