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:

looping.example.c#
 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 Output#
$ 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