File: intro.yo

package info (click to toggle)
c%2B%2B-annotations 12.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 13,044 kB
  • sloc: cpp: 24,337; makefile: 1,517; ansic: 165; sh: 121; perl: 90
file content (123 lines) | stat: -rw-r--r-- 6,465 bytes parent folder | download | duplicates (2)
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
Consider the following assignment: design a program that offers a function
tt(next) returning the next fibonacci number at subsequent
calls.footnote(Fibonacci numbers start with 0 and 1. The next fibonacci number
is the sum of the last two fibonacci numbers. The sequence, therefore, starts
with 0, 1, 1, 2, 3, 5, etc. In this and the following examples fibonacci
sequences are frequently used for illustration, and not because they are
inherently related to coroutines.)

Here is an example of how such a program could be designed: it defines a class
tt(Fibo) and in tt(main Fibo::next) is called to compute the next fibonacci
number (for brevity the program uses a single source file):
        verbinsert(-as4 demo/fibo/main.cc)

Clearly the tt(next) member isn't all that complicated. But when it's called
several actions are performed which themselves are unrelated to computing
fibonacci numbers:
    itemization(
    it() Calling a function boils down to (ignoring
            the complications when copying and destroying value type argument
            objects and value type local objects):
        itemization(
        it() pushing its arguments on the stack (note that tt(next) is a
            member function, so it em(does) have an argument: the address of
            the object for which tt(next) is called); 
        it() pushing the number of bytes required for those arguments on the
            stack; 
        it() pushing the location of the next instruction to execute after
            completing the tt(next) function call on the stack;
        it() pushing the location of the current function's stack frame on the
            stack;
        it() copying the current stack pointer's value to a register (commonly
            called the em(base pointer)), which register is then used to
            access tt(next's) arguments and local variables;
        it() continuing execution at tt(next's) first instruction;
        it() which first makes room on the stack for all of tt(next's)
            local variables.
       )
    it() once tt(next's return) statement has been executed:
        itemization(
        it() the stack pointer is reset to the value stored in the base
            pointer, thus removing local variables from the stack;
        it() popping the base pointer, thus restoring access to the caller's
            stack frame;
        it() popping the value of the caller's next instruction to execute
            from the stack;
        it() reducing the size of the stack by the value currently at the top
            of the stack, holding the size of the function's arguments.
        )
    )

These steps, although they look like a lot, in practice don't take that much
time, because most of them are performed by very fast register operations, and
the computer' architecture is usually highly optimized for these steps. 

Nonetheless, in situations where the functions themselves are short and simple
(like tt(Fibo::next)) these steps, requiring stack- and register
manipulations, might be considered unwelcome, raising the question whether
they may be avoided.

bf(C++) em(coroutines) allow us to avoid executing the steps that are required
when calling ordinary functions. The upcoming sections cover coroutines in
depth, but here is, for starters, the coroutine's equivalent of
tt(Fibo::next):
        label(FIBOCORO)
        verbinsert(-ans4 demo/fibocoro/fibocoro.cc)

Already now we can observe some characteristics of the tt(fiboCoro) coroutine:
    itemization(
    it() although coroutines em(can) be defined as class members, this
        particular coroutine is not. It's a free function, which therefore
        doesn't have a (hidden) pointer pointing to the object calling the
        coroutine;
    it() more importantly: it defines local variables (especially those in
        lines 5, 6), which em(keep their values) between successive
        calls. Such local variables are like static variables, but they're
        not. Each coroutine-call receives its own instances of its local
        variables;
    it() Lines 8 thru 16 define a continuous loop where each cycle computes
        the next fibonacci number;
    it() Once a fibonacci number is available the coroutine is em(suspended)
        (at line 15) without loosing the just computed values, passing tt(ret)
        to the coroutine's caller (the keyword ti(co_yield) is one of three
        new keywords which can be used by coroutines; the other two being
        ti(co_await) and ti(co_return)).
    )

While tt(fiboCoro) lays the foundation for obtaining a sequence of
fibonacci numbers, resuming a coroutine doesn't mean calling a function the
way the above member tt(Fibo::next) is called: there are no arguments; there
is no preparation of local variables; and there is no stack
handling. Instead there's merely a direct jump to the instruction just beyond
the coroutine's suspension point. When the coroutine's code encounters the
next suspension point (which occurs in tt(fiboCoro) at it's next cycle,
when it again reaches its tt(co_yield) statement) then the program's execution
simply jumps back to the instruction following the instruction that resumed
the coroutine.

The tt(main) function using the coroutine looks very similar to the tt(main)
function using the class tt(Fibo):
        verbinsert(-ans4 demo/fibocoro/main.cc)
    At line 5 tt(fiboCoro) is called, returning an (as yet not covered)
tt(fibo) thing. This tt(fibo) thing is called a hi(coroutine handle)
em(coroutine handler), and when (in line 14) tt(fibo.next()) is called, then
em(that) call resumes tt(fiboCoro), which is then again suspended at its
tt(co_yield) statement, returning the next available value through the
handler's tt(next) function.

The core feature of coroutines is thus that they may suspend their execution,
while keeping their current state (values of variables, location of the next
instruction to be executed, etc.). Normally, once suspended, the coroutine's
caller resumes its work beyond the instruction that returned the coroutine's
next value, so those two functions (the coroutine and its caller) closely
cooperate to complete the implemented algorithm. Co-routines are therefore
also known as
    emi(cooperating routines), which should not be confused with
em(concurrent) (multi-threaded) routines.

How this whole process works, and what its characteristics are, is covered in
the upcoming sections.