Hi,
I spent a bit of time over new years playing around with ‘fibers’.
This was partly in response to requests for me to look into async/await style functionality, but also touched on a bunch of other stuff I was interested in checking out such as coroutines/generators.
But just what is a fiber? The term is pinched from the win32 API, and they are in effect ‘lightweight’ threads. Like real threads, each fiber has it’s own stack pointer/program counter etc. However, unlike real threads, fibers don’t run ‘in parallel’. Only one fiber is ever running at a time, and a running fiber must explicitly ‘yield’ to allow another fiber to run.
My first stab at a fiber API was very simple and looked something like:
1
2
3
4
|
Function CreateFiber:Int( entry:Void() )
Function SwitchToFiber( fiber:Int )
Function GetCurrentFiber:Int()
|
With this alone, you can do some pretty cool stuff. For example, you could cause a fiber to ‘wait’ for the App.Idle signal, faking a ‘VWait’ of sorts:
1
2
3
4
5
6
7
8
|
Function VWait()
Local fiber:=GetCurrentFiber()
App.Idle+=Lambda()
SwitchToFiber( fiber )
End
SwitchToFiber( 0 )
End
|
(Note: fiber ‘0’ is reserved to mean the ‘GUI’ fiber…this should NEVER block!)
Probably looks a bit weird, but it’s actually pretty simple. All this does is add a ‘signal listener’ function to the App.Idle signal (which is emitted by the app framework when the app is idle) which, when called, wakes up the fiber that is VWaiting. Note also that VWait() will not work when called on the GUI fiber, as in this case the SwitchToFiber( 0 ) at the end becomes a NOP.
The messy bit is the switch back to fiber 0 at the end of VWait(). This is fine in a lot of situations, but not all. The simplest example of this is a fiber that starts another fiber that vwaits. In this case, the ‘child’ fiber shouldn’t just switch back to the gui fiber, but back to the ‘parent’ fiber, ie: the fiber that started it.
The problem here is that ‘pure’ fibers only really have 2 states – running or blocked. What’s really needed is a third state – ‘ready’. A ready fiber is not running, but it’s not blocked either – it wants to run again in the future. And it turns out this is actually quite easy to do, via the addition of 2 functions that work a bit like this:
1
2
3
4
5
6
7
8
9
|
Function ResumeFiber( fiber:Int )
readyStack.Push( GetCurrentFiber() )
SwitchToFiber( fiber )
End
Function SuspendCurrentFiber()
SwitchToFiber( readyStack.Pop() )
End
|
…where ‘readyStack’ is just a global stack of ints.
There are a few subtleties that mean these aren’t just implemented as wrappers around SwitchToFiber (in particular fiber creation/destruction) but that’s about it. Fibers don’t need an explicit ‘state’ var added to them, as their ‘ready’ state is implicit in whether they’re on the ready stack or not.
VWait looks almost the same…
1
2
3
4
5
6
7
8
|
Function VWait()
Local fiber:=GetCurrentFiber()
App.Idle+=Lambda()
ResumeFiber( fiber )
End
SuspendCurrentFiber()
End
|
…but can now be safely called from pretty much anywhere – except, still, the GUI fiber! In which case, SuspendCurrentFiber will attempt to pop an empty stack anyway, so again, do not block the GUI!
But when should you actually use fibers? Well, IMO one of the coolest uses for fibers is to ‘linearize’ game logic. If you check out the ‘defender’ source code in xmas monkey2 demo, you’ll notice a ton of ugly stuff in the ‘OnRender’ method for handling game state changes and dialog timers. This is due to the fact that the app is callback based, and callbacks must return promptly and without blocking for everything to keep going, so you’re stuck with having to update a bunch of state vars incrementally every tick.
However, with fibers you can do the sequential game logic stuff in a much more natural manner. The latest version of defender now has a ‘GameLoop’ method that looks something like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Method GameLoop()
Repeat
ShowIntroDialog()
StartNewGame()
PlayGame()
ShowGameOverDialog()
Forever
End
|
This code is run on a fiber using CreateFiber( GameLoop ) and as such, it can freely block (eg: vwait) without blocking the GUI thread. The dialogs can therefore ‘wait 10 seconds’, PlayGame() can VWait etc without any problem. All in all, it’s suspiciously like coding in b3d/bmx again!
Another use for fibers is in writing ‘generator’ style code. In fact, it is remarkably easy to build a generic generator class with fibers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
Class Generator<T>
Property HasNext:Bool()
Return _hasNext
End
Method GetNext:T()
Local value:=_next
ResumeFiber( _fiber )
Return value
End
Protected
Method Start( entry:Void() )
_fiber=StartFiber( Lambda()
_hasNext=True
entry()
_hasNext=False
End )
End
Method Yield( value:T )
_next=value
SuspendCurrentFiber()
End
Private
Field _fiber:Int
Field _hasNext:Bool
Field _next:T
End
|
…and a simple example ‘int range’ generator…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
Class IntGenerator Extends Generator<Int>
Method New( min:Int,max:Int )
Start( Lambda()
For Local i:=min To max
Yield( i )
Next
End )
End
End
Function Main()
Local gen:=New IntGenerator( 1,10 )
While gen.HasNext
Print gen.GetNext()
Wend
End
|
Finally, there’s a catch: fibers don’t currently work in emscripten. There are features being added to emscripten that means they may eventually happen, and I have had a demo going (only works on firefox ‘nightly’ though) although it was a bit limited (you could only start a fiber from the main/gui fiber) but realistically I don’t think fibers in emscripten will be seriously viable in the near future.
But one of my ‘conditions’ for providing emscripten support was that I was NOT going to let it hold back the other targets, and this is a very good case in point. So monkey2 will eventually have some sort of fiber support, but probably only for non-emscripten targets. Bummer, but what can ya do…
Bye!
Mark
Collaborative multitasking without the headaches of interrupts
Very useful in software we build ourselves indeed!
I think with web based stuff still up in the air and some of it in its infancy support for it should never hold back the main MonkeyX2 development.