Finish fiber docs.

This commit is contained in:
Bob Nystrom
2014-04-19 20:33:49 -07:00
parent 3087daaece
commit d338ef2f07

View File

@ -5,11 +5,9 @@ Fibers are a key part of Wren. They form its execution model, its concurrency st
They are a bit like threads except they are *cooperatively* scheduled. That means Wren won't stop running a fiber and switch to another until you tell to. You don't have to worry about context switches at random times and all of the headaches those cause.
Fibers in Wren are dramatically more expressive than generators in Python and C#. They are on par with coroutines in Lua (though Wren supports both symmetric and asymmetric coroutines while Lua only directly has the latter).
They are scheduled entirely by Wren, so they don't use OS thread resources, or require heavyweight context switches. They just need a bit of memory for their stacks. A fiber will get garbage collected like any other object when not referenced any more, so you can create them freely.
Fibers are lightweight and efficient in Wren. They are scheduled entirely by Wren, so they don't use OS thread resources, or require heavyweight context switches. They just need a bit of memory for their stacks. A fiber will get garbage collected like any other object when not referenced any more, so you can create them freely.
My goal is to keep them lightweight enough that you can safely, for example, have a separate fiber for each entity in a game. Wren should be able to handle thousands of them without any trouble. For example, when you run Wren in interactive mode, it creates a new fiber for every line of code you type in.
They are lightweight enough that you can, for example, have a separate fiber for each entity in a game. Wren can handle thousands of them without any trouble. For example, when you run Wren in interactive mode, it creates a new fiber for every line of code you type in.
## Creating fibers
@ -22,28 +20,28 @@ All Wren code runs within the context of a fiber. When you first start a Wren sc
Creating a fiber does not immediately run it. It's just a first class bundle of code sitting there waiting to be activated, a bit like a [function](functions.html).
## Running fibers
## Invoking fibers
Once you've created a fiber, you can run it (which suspends the current fiber) by calling its `run` method:
Once you've created a fiber, you can invoke it (which suspends the current fiber) by calling its `call` method:
:::dart
fiber.run
fiber.call
The run fiber will then execute its code until it reaches the end of its body or until it passes control to another fiber. If it reaches the end of its body, it's considered *done*:
The called fiber will execute its code until it reaches the end of its body or until it passes control to another fiber. If it reaches the end of its body, it's considered *done*:
:::dart
var fiber = new Fiber { IO.print("Hi") }
fiber.isDone // false
fiber.run
fiber.call
fiber.isDone // true
When it finishes, it will automatically resume the fiber that ran it. This works like coroutines in Lua. It's a runtime error to try to run a fiber that is already done.
When it finishes, it automatically resumes the fiber that called it. It's a runtime error to try to call a fiber that is already done.
## Yielding
The main difference between fibers and functions is that a fiber can be suspended in the middle of its operation and then resumed later. Running another fiber is one way to suspend a fiber, but that's more or less the same as one function calling another.
The main difference between fibers and functions is that a fiber can be suspended in the middle of its operation and then resumed later. Calling another fiber is one way to suspend a fiber, but that's more or less the same as one function calling another.
Things get interesting when a fiber *yields*. A yielded fiber passes control *back* to the fiber that ran it, but *remembers where it is*. The next time the fiber is run, it picks up right where it left off and keeps going.
Things get interesting when a fiber *yields*. A yielded fiber passes control *back* to the fiber that ran it, but *remembers where it is*. The next time the fiber is called, it picks up right where it left off and keeps going.
You can make a fiber yield by calling the static `yield` method on `Fiber`:
@ -55,9 +53,9 @@ You can make a fiber yield by calling the static `yield` method on `Fiber`:
}
IO.print("main 1")
fiber.run
fiber.call
IO.print("main 2")
fiber.run
fiber.call
IO.print("main 3")
This program prints:
@ -71,10 +69,51 @@ This program prints:
Note that even though this program has *concurrency*, it's still *deterministic*. You can reason precisely about what it's doing and aren't at the mercy of a thread scheduler playing Russian roulette with your code.
**TODO: show example that can't do with generators**
## Passing values
**TODO: pass values to run, yield**
Calling and yielding fibers is used for passing control, but it can also pass *data*. When you call a fiber, you can optionally pass a value to it. If the fiber has yielded and is waiting to resume, the value becomes the return value of the `yield` call:
## Symmetric concurrency
:::dart
var fiber = new Fiber {
var result = Fiber.yield
IO.print(result)
}
**TODO**
fiber.call("discarded")
fiber.call("sent")
This prints "sent". Note that the first value sent to the fiber through call is ignored. That's because the fiber isn't waiting on a `yield` call, so there's no where for the sent value to go.
Fibers can also pass values *back* when they yield. If you pass an argument to `yield`, that will become the return value of the `call` that was used to invoke the fiber:
:::dart
var fiber = new Fiber {
Fiber.yield("sent")
}
IO.print(fiber.call)
This also prints "sent".
## Full coroutines
What we've seen so far is very similar to what you can do with languages like Python and C# that have *generators*. Those let you define a function call that you can suspend and resume. When using the function, it appears like a sequence you can iterate over.
Wren's fibers can do that, but they can do much more. Like Lua, they are full *coroutines*—they can suspend from anywhere in the callstack. For example:
:::dart
var fiber = new Fiber {
(1..10).map {|i|
Fiber.yield(i)
}
}
Here, we're calling `yield` from within a [function](functions.html) being passed to the `map` method. This works fine in Wren because that inner `yield` call will suspend the call to `map` and the function passed to it as a callback.
## Transferring control
Fibers have one more trick up their sleeves. When you execute a fiber using `call`, the fiber tracks which fiber it will return to when it yields. This lets you build up a chain of fiber calls that will eventually unwind back to the main fiber when all of the called ones yield or finish.
This works fine for most uses, but sometimes you want something a little more freeform. For example, you may be creating a [state machine](http://en.wikipedia.org/wiki/Finite-state_machine) where each state is a fiber. When you switch from one state to the next, you *don't* want to build an implicit stack of fibers to return to. There is no "returning" in this case. You just want to *transfer* to the next fiber and forget about the previous one entirely. (This is analogous to [tail call elimination](http://en.wikipedia.org/wiki/Tail_call) for regular function calls.)
To enable this, fibers also have a `run` method. This begins executing that fiber, and "forgets" the previous one. If the running fiber yields or ends, it will transfer control back to the last *called* one. (If there are no called fibers, it will end execution.)