Creating Asynchronous Loops#
The control flow loop is one of the most fundamental programming constructs. The
abstractions in amongoc
let you define asynchronous loops, where each loop
step is itself an asynchronous operation. The core API that enables looping is
amongoc_let()
.
The Example Program#
The behavior of the sample program is very simple: Accept a positive integer as a command line argument, and delay for that many seconds, printing a message and accumulating a sum each second as the countdown runs. This guide will explain the following program:
1#include <amongoc/amongoc.h>
2
3#include <stdio.h>
4#include <stdlib.h>
5
6/**
7 * @brief State for the program
8 */
9typedef struct {
10 // The countdown to zero
11 int countdown;
12 // The event loop for the operation
13 amongoc_loop* loop;
14 // Fibonacci state
15 uint64_t a, b;
16} state;
17
18/** loop_step()
19 * @brief Handle each step of our looping operation
20 *
21 * @param state_ptr Pointer to the `state` for the program
22 */
23amongoc_emitter loop_step(amongoc_box state_ptr, amongoc_status prev_status, amongoc_box prev_res) {
24 (void)prev_res;
25 (void)prev_status;
26 // Print our status
27 state* s = amongoc_box_cast(state*, state_ptr);
28 // Compute the next sum and shift our numbers
29 uint64_t cur = s->a;
30 uint64_t sum = s->a + s->b;
31 s->a = s->b;
32 s->b = sum;
33 fprintf(stderr, "%d seconds remain, current value: %lu\n", s->countdown, cur);
34 // Check if we are done
35 if (s->countdown == 0) {
36 // No more looping to do. Return a final null result
37 return amongoc_just(amongoc_okay,
38 amongoc_box_uint64(cur),
39 amongoc_loop_get_allocator(s->loop));
40 }
41 // Decrement the counter and start a sleep of one second
42 --s->countdown;
43 struct timespec dur = {.tv_sec = 1};
44 amongoc_emitter em = amongoc_schedule_later(s->loop, dur);
45 // Connect the sleep to this function so that we will be called again after
46 // the delay has elapsed. Return this as the new operation for the loop.
47 return amongoc_let(em,
48 amongoc_async_forward_errors,
49 amongoc_loop_get_allocator(s->loop),
50 state_ptr,
51 loop_step);
52}
53// end.
54
55int main(int argc, char const* const* argv) {
56 // Parse a delay integer from the command-line arguments
57 if (argc != 2) {
58 fprintf(stderr, "Usage: %s <delay>\n", argv[0]);
59 return 2;
60 }
61 int delay = atoi(argv[1]);
62 if (((delay == 0) && strcmp(argv[1], "0")) || delay < 0) {
63 fprintf(stderr, "Expected <delay> to be a positive integer\n");
64 return 2;
65 }
66
67 // Create a default loop
68 amongoc_loop loop;
69 amongoc_default_loop_init(&loop);
70
71 // Seed the initial sum
72 state app_state = {.countdown = delay, .loop = &loop, .a = 0, .b = 1};
73
74 // Start the loop
75 amongoc_emitter em = loop_step(amongoc_box_pointer(&app_state), amongoc_okay, amongoc_nil);
76 // Tie the final result for later, and start the program
77 amongoc_status status;
78 amongoc_box result;
79 amongoc_operation op = amongoc_tie(em, &status, &result, mlib_default_allocator);
80 amongoc_start(&op);
81 // Run the program within the event loop
82 amongoc_default_loop_run(&loop);
83 amongoc_operation_delete(op);
84 amongoc_default_loop_destroy(&loop);
85
86 if (amongoc_is_error(status)) {
87 char* msg = amongoc_status_strdup_message(status);
88 fprintf(stderr, "error: %s\n", msg);
89 free(msg);
90 amongoc_box_destroy(result);
91 return 2;
92 } else {
93 // Get the value returned with `amongoc_just` in `loop_step`
94 printf("Got final value: %lu\n", amongoc_box_cast(uint64_t, result));
95 }
96 return 0;
97}
Initiating the Program#
We declare our shared app state as a struct type. This is passed through the program using a pointer:
9typedef struct {
10 // The countdown to zero
11 int countdown;
12 // The event loop for the operation
13 amongoc_loop* loop;
14 // Fibonacci state
15 uint64_t a, b;
16} state;
After basic command-line argument processing, we initialize the event loop and application state for the program:
67 // Create a default loop
68 amongoc_loop loop;
69 amongoc_default_loop_init(&loop);
70
71 // Seed the initial sum
72 state app_state = {.countdown = delay, .loop = &loop, .a = 0, .b = 1};
In the sample program, the looping operation is initiated here:
74 // Start the loop
75 amongoc_emitter em = loop_step(amongoc_box_pointer(&app_state), amongoc_okay, amongoc_nil);
We pass amongoc_okay
and amongoc_nil
as the initial “result” values. We pass
a pointer to our application state as the first argument using
amongoc_box_pointer()
.
Loop Stepping#
In this example, the loop_step
function is called both initialy and
subsequently for each sub-operation. The signature is written in such a way
that it is valid as an amongoc_let_transformer
function. It is possible that
your program will need a separate function to initiate your loop.
18/** loop_step()
19 * @brief Handle each step of our looping operation
20 *
21 * @param state_ptr Pointer to the `state` for the program
22 */
23amongoc_emitter loop_step(amongoc_box state_ptr, amongoc_status prev_status, amongoc_box prev_res) {
24 (void)prev_res;
25 (void)prev_status;
Our loop stepping function is called both to initiate the loop and to continue
the loop after the sub-operation has completed. Because our initiation and our
operation (amongoc_schedule_later()
) both give us amongoc_nil
, we do not need
to handle the result value (passed as the third parameter) and we can simply
discard it.
Printing a Mesage & Accumulating a Sum#
We then extract our application state, perform some arithmetic, and print a progress message to the user.
26 // Print our status
27 state* s = amongoc_box_cast(state*, state_ptr);
28 // Compute the next sum and shift our numbers
29 uint64_t cur = s->a;
30 uint64_t sum = s->a + s->b;
31 s->a = s->b;
32 s->b = sum;
33 fprintf(stderr, "%d seconds remain, current value: %lu\n", s->countdown, cur);
This will assure the user that the application is running.
Checking the Stop Condition#
While we could loop forever, our program only wants to wait a limited amount
of time. We use a branch and amongoc_just()
to terminate the loop when our
countdown reaches zero.
35 if (s->countdown == 0) {
36 // No more looping to do. Return a final null result
37 return amongoc_just(amongoc_okay,
38 amongoc_box_uint64(cur),
39 amongoc_loop_get_allocator(s->loop));
40 }
The amongoc_just()
function creates a pseudo-async operation that resolves
immediately with the given result. Here, we create a successful status with
amongoc_okay
and use amongoc_box_uint64()
to create a box that stores the
final calculation. This result value box will appear at the end of our loop.
Starting a Timer#
If our countdown is not finished, we use amongoc_schedule_later()
to start
another delay:
41 // Decrement the counter and start a sleep of one second
42 --s->countdown;
43 struct timespec dur = {.tv_sec = 1};
44 amongoc_emitter em = amongoc_schedule_later(s->loop, dur);
amongoc_schedule_later()
creates an operation that will start a timer for the
given duration and then complete with amongoc_nil
when the timer fires.
Continuing the Loop#
The core looping behavior is handled by amongoc_let()
, a building-block of
asynchrony that creates allows the completion of an asynchronous operation to
initiate a new operation to continue the program:
45 // Connect the sleep to this function so that we will be called again after
46 // the delay has elapsed. Return this as the new operation for the loop.
47 return amongoc_let(em,
This connects the completion of the timeout from amongoc_schedule_later()
to a
continuation via loop_step
. The operation returned by loop_step
becomes
the result of the emitter returned by amongoc_let()
.
Since our loop_step
can return another operation initiated by amongoc_let()
,
the emitter from amongoc_let()
will loop back on itself until the returned
emitter resolves in some other fashion (in our case, amongoc_just()
, or if
there is an error condition, specified by amongoc_async_forward_errors
).
Starting and Running the Loop#
Our looping operation has a final result value, so we will want to create and
tie storage for it. We do this using amongoc_tie()
, which will also create the
final amongoc_operation
from the looping emitter we created from initiating
the loop.
76 // Tie the final result for later, and start the program
77 amongoc_status status;
78 amongoc_box result;
79 amongoc_operation op = amongoc_tie(em, &status, &result, mlib_default_allocator);
amongoc_tie()
will attach a handler to the emitter that will store the final
result and status in the pointed-to location when the operation completes.
Starting the Operation#
When next execute amongoc_start()
on the returned operation object:
80 amongoc_start(&op);
This will launch the operation on the event loop. The program isn’t running yet, though, as the default event loop requires a thread to execute it.
Running the Loop and Finalizing the Operation#
81 // Run the program within the event loop
82 amongoc_default_loop_run(&loop);
83 amongoc_operation_delete(op);
84 amongoc_default_loop_destroy(&loop);
The call to amongoc_default_loop_run()
will execute the operations stored in
the event loop and return when all operations are complete. We are then safe to
destroy the operation with amongoc_operation_delete()
, and we can discard the
event loop with amongoc_default_loop_destroy()
.
Error Handling and Printing the Final Result#
After the emitter used with amongoc_tie()
has completed, its final result
status and value are stored in the pointed-to storage, ready to be read by the
program. We check for errors, either printing the error message or printing the
final result:
86 if (amongoc_is_error(status)) {
87 char* msg = amongoc_status_strdup_message(status);
88 fprintf(stderr, "error: %s\n", msg);
89 free(msg);
90 amongoc_box_destroy(result);
91 return 2;
92 } else {
93 // Get the value returned with `amongoc_just` in `loop_step`
94 printf("Got final value: %lu\n", amongoc_box_cast(uint64_t, result));
95 }
96 return 0;
We use amongoc_is_error()
to test the final status for an error condition. If it
is an error, we get and print the error message to stderr, and we must destroy
the final result box because it may contain an unspecified value related to the
error, but we don’t want to do anything with it.
In the success case, we extract the value returned in amongoc_just()
as a
uint64_t
and print it to stdout. Note that because the box returned by
amongoc_box_uint64()
is trivial, we can safely discard the
box without destroying it.
$ looping.example.exe 15
15 seconds remain, current value: 0
14 seconds remain, current value: 1
13 seconds remain, current value: 1
12 seconds remain, current value: 2
11 seconds remain, current value: 3
10 seconds remain, current value: 5
9 seconds remain, current value: 8
8 seconds remain, current value: 13
7 seconds remain, current value: 21
6 seconds remain, current value: 34
5 seconds remain, current value: 55
4 seconds remain, current value: 89
3 seconds remain, current value: 144
2 seconds remain, current value: 233
1 seconds remain, current value: 377
0 seconds remain, current value: 610
Got final value: 610