Future & Promise
Future & Promise
Introduction
Intro to Asynchronous Programming
Asynchronous programming is needed when you don't know when an event will happen and you can't block the whole program execution to wait for it. Instead you continue your program execution and wait for the event to happen asynchronously -- at some point in time the system will alert you about it, this is usually referred as call back.
The way the system will alert you (call you back) varies amongst implementations. At the very basic level CPUs will do that using interruption services and interruption service handlers (callback specification). Interruptions are preemptive: your application execution will be stopped at an unpredictable state, then you handle it and exit, resuming execution. Since it's unpredictable, it's hard to use and Operating Systems usually abstract that for you.
With UNIX Operating Systems you can get notified in your process using two methods:
- Signals: mimic interruptions as they are preemptive, thus unpredictable and painful to use.
- Poll/Select: one specifies a series of possible events it's interested in and then "sleeps" waiting for one of those events to happen. It's a voluntary and thus will happen in a predictable fashion, very easy to use.
EFL (Ecore) uses poll/select as core for its main loop, converting signals in a safe way. Programmers will register events (timers, file descriptors, jobs, events and signals) and these will be posted to a queue. Once execution returns to the main loop (ie: all the callback stack for one event), then the next event is processed and if there is a handler (callback specification), then the programmer's code is called back. It's important to note that it's all cooperative, the programmer's code is never interrupted. This means that when you ask for a "timeout in 1.0", it will likely be delayed a little bit, since another event callback may be doing its processing and can't be interrupted! Thus to cooperate well and keep system responsive*, these callback functions must be short-lived**, usually with time boundary that keeps the User Interface (or system in general) responsive -- a "frame time" (usually 1/60 seconds).
Cooperative Tasks
QUESTION: How to keep functions short lived if you may need to do processing that takes multiple frame times?
ANSWER: You segment your work into multiple pieces and go back to the main loop after a reasonable time (ie: usually smaller than a frame time)
EXAMPLE: whenever you have to operate on an unbounded number of items (thus potentially larger than frame time), you may segment the work in units you know will fit in a frame time, registering for a "job" (or "idler", depending on the priority of your work related to others) to handle the rest:
void my_event_handler(my_ctx *ctx) { int i; for (i = 0; i < N; i++) do_something(ctx); }
void my_event_handler(my_ctx *ctx) { int i; for (i = ctx->start; i < N && (i - ctx->start) < 5; i++) do_something(ctx); if (i < N) schedule_new_event(ctx); // still work to do... }
Chaining Problem
This example was very simple on purpose, however in many cases you'd be chaining multiple components, such as wait for an image to be loaded asynchronously (disks may be slow), then execute some heavy operation in a thread, then save it to disk asynchronously. While this is very simple to describe in text, it's not so simple to see in traditional C code as it's not condensed in a single place, rather scattered across multiple callbacks! It would look like:
void on_image_load(my_ctx *ctx, img *img) { ctx->img_load_async_handle = NULL; ctx->img_async_operation_handle = img_async_operation(img, on_image_async_operation, ctx); if (!ctx->img_async_operation_handle) ctx->on_error(ctx, "image async operation failed to start"); } void on_image_save(my_ctx *ctx, img *img) { ctx->img_save_async_handle = NULL; ctx->on_success(ctx); } void on_image_async_operation(my_ctx *ctx, img *img) { ctx->img_async_operation_handle = NULL; ctx->img_save_async_handle = img_save(img, ctx->save_filename, on_image_save, ctx); if (!ctx->img_save_async_handle) ctx->on_error(ctx, "image save failed to start"); } void on_error(my_ctx *ctx, const char *msg) { fprintf(stderr, "ERROR: %s\n", msg); exit(1); } void on_success(my_ctx *ctx) { exit(0); } void on_keyboard_interruption(my_ctx *ctx) { if (ctx->img_load_async_handle) img_load_cancel(ctx->img_load_async_handle); if (ctx->img_async_operation_handle) img_async_operation_cancel(ctx->img_async_operation_handle); if (ctx->img_save_async_handle) img_save_cancel(ctx->img_save_async_handle); unlink(ctx->save_filename); exit(1); } int main(int argc, char *argv[]) { my_ctx *ctx = my_ctx_new(on_success, on_error); ctx->save_filename = argv[2]; img_load(argv[1], on_image_load, ctx); a_main_loop_keyboard_interrupt_set(on_keyboard_interruption, ctx); a_main_loop_run(); return 0; }
See that is hard to find what's happening after img_load(), you need to go to its callback to see -- note that the callbacks were declared in a mixed order so you don't guess, it's a mistake to think code will be organized in a multi-person project.
Also see that the error handling is painfully spread, as well as cancellation. It's referred as Callback Hell as it's hard to visualize, it's easy to get wrong due spread error handling and cancellation.
We should be able to describe that more easily and clearly.
Promises and Futures
This is what Promises and Futures are about: to describe a chain of asynchronous events. It can be understood like a pipe, a flow of information. In many places it's only referred as Promises, however in C/EFL we'll handle them as 2 distinct roles:
- Promise is a "value pending resolution", that is. It's a promise that you'll either get a value or an error. That's the only possible lifecycle of a promise: either you resolve (fulfill) or you reject it, then it's gone! You can't resolve or reject it more than once, and you must resolve it once to destroy it (even if it's with an error, such as ECANCELED)
- Future is a callback that specifies what to do with a resolved value. They're chain-able, also referred as then-able, and they must pass thru a received value or produce new values for the next element in the chain, including produce new promises, that will wait to be resolved before the next future is called.
The value received by a future can be translated to something else, including a different type. This includes errors, which can be handled and converted to non-errors. Examples:
- A promise to query the database may return a set of rows (ie: RowSet). A future can convert that into a single row (ie: Row), followed by another future that converts that to a cell (ie: Cell).
- A promise to open a file in the disk can return an error (ENOENT). A future may handle that error and instead create a new file, propagating either the file OR another error.
Futures are not mandatory. Whenever a promise is returned and nobody adds a future for it, it will still resolve asynchronously, however nobody will be called back, as there are no callbacks attached.
Given our earlier examples, written as promises and futures:
void *on_image_load(my_ctx *ctx, img *img) { return img_async_operation(img); } void *on_image_save(my_ctx *ctx, img *img) { return NULL; } void *on_image_async_operation(my_ctx *ctx, img *img) { return img_save(img, ctx->save_filename); } void *on_error(my_ctx *ctx, const char *msg) { fprintf(stderr, "ERROR: %s\n", msg); unlink(ctx->save_filename); exit(1); } void *on_success(my_ctx *ctx, const void *value) { ctx->future = NULL; exit(0); } void on_keyboard_interruption(my_ctx *ctx) { if (ctx->future) future_cancel(ctx->future); exit(1); } int main(int argc, char *argv[]) { my_ctx *ctx = my_ctx_new(on_success, on_error); ctx->save_filename = argv[2]; ctx->future = future_chain(img_load(argv[1]), { on_img_load, NULL, ctx }, { on_img_async, NULL, ctx }, { on_img_save, NULL, ctx }, { on_success, on_error, ctx }); a_main_loop_keyboard_interrupt_set(on_keyboard_interruption, ctx); a_main_loop_run(); return 0; }
Promises/A+
Promises gained lots of traction in JavaScript as it's widely used and the "callback hell" problem started to be a problem for not-so-skilled developers (after all, it's very easy to get the "callback hell" wrongly). They even created a very good specification called Promises/A+.
JavaScript particularities aside, what it says is:
- a promise can be resolved or rejected only once. It's not possible to reject a previously resolved, neither resolve a previously rejected promise.
- futures callbacks must be dispatched in a clean stack, in their terms "execution context stack contains only platform code". In our terms: it's called directly from the main loop.
Since future callbacks will always go to the main loop, you can expect the following code:
Eina_Value cb_printing_B(void *data, const Eina_Value value, const Eina_Future *dead_future) { if (value.type == EINA_VALUE_TYPE_INT) { /* we're in C, always check the type prior to eina_value_get()! */ int i; if (eina_value_get(&value, &i)) /* good practice */ printf("B is printed later, when main loop executes: value is %d\n", i); } else if (value.type == EINA_VALUE_TYPE_ERROR) { /* always handle errors! */ Eina_Error err; if (eina_value_get(&value, &err)) /* good practice */ fprintf(stderr, "ERROR: %d (%s)\n", err, eina_error_msg_get(err)); } return value; /* pass thru */ } void test(void) { Eina_Promise *p = eina_promise_new(...); eina_future_then(eina_future_new(p), cb_printing_B, NULL); eina_promise_resolve(p, eina_value_value_from_int(123)); /* futures will be dispatched later, from main loop */ printf("A is printed first\n"); }
EFL Promises and Futures were implemented following Promises/A+ as close as possible, with a key difference:
That is, once eina_future_cancel() is called then all Eina_Future_Cb will be called with ECANCELED in the current context/stack. Likewise, if any Eina_Promise were pending resolution, its cancel is called in the current context/stack. It's not going back to the main loop to do so!
This behavior is required since, unlike JavaScript, our core is in C and not reference counted. You may want to cancel the callback and right away free(ctx) they could use. That said, if error is ECANCELED, then you may be called back from an unsafe context.
Another difference is that JavaScript uses 2 callbacks in "then", one for success and another for error. Since our core is in C and one must always check value type prior to its usage otherwise you may get segmentation fault, the core uses a single callback and the user is expected to check if it's an EINA_VALUE_TYPE_ERROR or something he expects. A nice side effect is that our core is very lightweight on memory, shaving some code and pointers. However we offer eina_future_cb_easy() that will handle type checking for you and offer 3 callbacks: success, error and free (always called), this can be used with eina_future_then_from_desc() (eina_future_then() is a macro that calls it) or eina_future_chain().
EFL Implementation
EFL promise and future are very lightweight on dependencies and implemented at Eina layer. However each Eina_Promise that is created should provide an Eina_Future_Scheduler that is used to schedule the future delivery in a safe context.
Whenever using the full EFL stack, this is usually the Ecore main loop and the scheduler should be fetched from Efl.Loop object using efl_loop_future_scheduler_get() (read-only Eo property: future_scheduler). This will cope with multiple main loops, each promise can be bound to a specific main loop.
Our implementation is very lightweight, namely:
Type | 32 bits | 64 bits |
---|---|---|
Eina_Promise1 | 16 bytes | 32 bytes |
Eina_Future1 | 28 bytes | 56 bytes |
Eina_Future_Cb_Easy | 48 bytes (28 + 20) | 96 bytes |
Efl_Future_Cb | 48 bytes (28 + 20) | 96 bytes |
Eina_Value2 | 12 bytes | 16 bytes |
1 These are allocated out of Eina_Mempool to avoid pressure on memory allocator and fragmentation.
2 Eina_Value is usually passed as value, thus not allocate but uses the stack. For complex types such as strings, stringshare, arrays or structures it may allocate more memory.
Promise
This is the write-side of the pipe, once created it must be either eina_promise_resolve() or eina_promise_reject() to finalize. During its created one must specify a cancel callback, which is responsible to abort the pending process, or at least detach it so when it finishes it won't use any resources that could lead to a crash -- including the cancelled Eina_Promise! If the process cannot be aborted, then at least store the Eina_Promise * somewhere and have cancel to turn it NULL, once the process finishes check that pointer to see if it's still valid.
Once resolved, the value is owned by the promise delivery system. It's not eina_value_copy()ed, just the pointers will be kept alive until the value is dispatched to some future or there are no more futures. The value can be passed thru futures unchanged, in this case it's kept alive until the first future that returns a new value. Just then the eina_value_flush() will be called.
Future
This is the read-side of the pipe. A future is either created for a promise using efl_future_new() or for another future, also known as "chain" or "then", using efl_future_then(). A helper is provided to nicely create a chain without too much nesting that would be required in C (eina_future_chain()), instead taking a NULL terminated array of callbacks.
There are set of helpers such as eina_value_int_init() that will setup the value for a given type and set its contents, these are useful to have one-line returns for basic types.
Racing Futures: winner takes all (Race)
A common pattern for futures is to run many in parallel and once the first resolves, cancel the rest. For instance one can create a task and then a timeout, race both and get the task cancelled if takes too long -- if it resolves soon enough, timeout is automatically cancelled.
static Eina_Value finished(void *data EINA_UNUSED, const Eina_Value value, const Eina_Future *dead_future EINA_UNUSED) { if (value.type == EINA_VALUE_TYPE_ERROR) { // always handle error (ie: ENOMEM, ECANCELED...) Eina_Error err; eina_value_get(&value, &err); fprintf(stderr, "ERROR: finished with #%d %s\n", err, eina_error_msg_get(err)); } else if (value.type == EINA_VALUE_TYPE_STRUCT) { unsigned int idx; Eina_Value result; // race result is a struct with "index" and "value" members if (eina_value_struct_get(&value, "index", &idx) && eina_value_struct_get(&value, "value", &result)) { char *str = eina_value_to_string(&result); printf("Future %s won! Result: %s\n", idx == 0 ? "download" : "timeout", str); free(str); } else { fprintf(stderr, "ERROR: failed to fetch race result members!\n"); return eina_value_error_init(EINVAL); } } return value; // pass thru the results } void init(Efl_Object *loop) { Eina_Future *download = do_download(loop, "http://www.enlightenment.org"); Eina_Future *timeout = efl_loop_timeout(loop, 10.0); eina_future_then(eina_future_race(download, timeout_future), .cb = finished); // finished is called once download or timeout resolve (the first one) }
Waiting many futures (All)
Another common pattern for futures is to run many in parallel and wait all of them to resolve, then provide an array of resolved values. Note that the values are in the same order as the promises.
static Eina_Value finished(void *data EINA_UNUSED, const Eina_Value value, const Eina_Future *dead_future EINA_UNUSED) { if (value.type == EINA_VALUE_TYPE_ERROR) { // always handle error (ie: ENOMEM, ECANCELED...) Eina_Error err; eina_value_get(&value, &err); fprintf(stderr, "ERROR: finished with #%d %s\n", err, eina_error_msg_get(err)); } else if (value.type == EINA_VALUE_TYPE_ARRAY) { unsigned int i, len; len = eina_value_array_count(&value); for (i = 0; i < len; i++) { Eina_Value item; if (eina_value_array_get(&value, i, &item)) { char *str = eina_value_to_string(&item); printf("Value at #%u: %s\n", i, str); free(str); } else { fprintf(stderr, "ERROR: failed to fetch value #%u\n", i); } } } return value; // pass thru the results } void init(Efl_Object *loop) { Eina_Future *download1 = do_download(loop, "http://www.enlightenment.org"); Eina_Future *download2 = do_download(loop, "http://www.google.com"); eina_future_then(eina_future_all(download1, download2), .cb = finished); // finished is called once all downloads resolve }
Helper Future Callbacks
Eina_Future_Desc allows for helper callbacks to be generated and passed easily to eina_future_then_from_desc(), eina_future_chain() or eina_future_chain_array(). These may allocate data and return in Eina_Future_Desc::data without worries as the protocol will always call Eina_Future_Desc::cb, even on errors or when the future is cancelled -- thus no leaks should occur.
Type Convert
eina_future_cb_convert_to(type) returns an Eina_Future_Desc that will convert the future result to the given type.
eina_future_chain(something_that_produces_a_future_int(), eina_future_cb_convert_to(EINA_VALUE_TYPE_STRING));
Print to console
eina_future_cb_console_from_desc() or its syntax sugar eina_future_cb_console() returns an Eina_Future_Desc that will print out the results to stdout with an optional prefix and suffix. Received value will be passed thru unchanged.
eina_future_chain(something_that_produces_a_future(), eina_future_cb_console(), // default prefix ("") and suffix ("\n") eina_future_cb_console(.prefix = "something produced: "), // named parameter eina_future_cb_console("something produced: "), // positional parameter eina_future_cb_console(.suffix = ", is it right?\n"), // named parameter, include "\n" if it's needed! eina_future_cb_console(NULL, ", is it right?\n"), // positional parameter, include "\n" if it's needed! );
Easy Callbacks
Eina_Future_Desc is lean and keeps core very efficient and simple. However sometimes users want to avoid cumbersome tasks such as check for error and differentiate it from regular value, validate expected success type and so on. Then eina_future_cb_easy_from_desc() uses Eina_Future_Cb_Easy_Desc to provide more details, resulting in a wrapper callback that is every easy to use. Syntax sugar exists as eina_future_cb_easy() to be used with eina_future_then_from_desc() or eina_future_chain(); eina_future_then_easy() and eina_future_chain_easy() will make it even simpler if you want an even simpler path.
eina_future_chain_easy(something_that_produces_a_future(), { .success = just_success_cb, .data = some_data }, { .error = just_error_cb }, { .free = just_monitor_and_free, .data = other_data }, { .success = success_if_type_is_int, .success_type = EINA_VALUE_TYPE_INT }, { .success_type = EINA_VALUE_TYPE_INT }); // only success_type will enforce type or convert to EINVAL error
Efl_Object (Eo) integration
Binding Future to an Object
Usually a promise or future must be bound to an object life, be the owner object responsible to resolve it or some user that want to bind (link) to its own lifecycle.
This is done with the Eina_Future_Desc returned by efl_future_cb_from_desc() or its syntax sugar macro efl_future_cb() which mimics eina_future_cb_easy() behavior, however instead of a general void *data it uses an Efl_Object *o and binding to its lifecycle. As usual, this Eina_Future_Desc can be used with eina_future_then_from_desc() or eina_future_chain(). If more than one is to be used, then the helper efl_future_chain_from_array() or efl_future_chain() (syntax sugar) can be used as listed below.
When efl_future_cb_from_desc() or variants are used, the given Eina_Future will be chained to a new one that will:
- monitor object death, once the object destructor runs all pending futures will be cancelled with eina_future_cancel();
- monitor future resolution, once the future resolves, it's removed from pending list;
- if success_type is provided, then check for resolved Eina_Value::type == success_type, if not convert to EINA_VALUE_TYPE_ERROR (EINVAL);
- dispatch success callback, if provided;
- dispatch error callback, if provided and Eina_Value::type == EINA_VALUE_TYPE_ERROR;
- dispatch free callback, if provided;
- if storage is provided, it's set once the future is created and reset to NULL before callback is called. Users shouldn't have to worry about things like pd->my_future.
Use case: an object creates a promise and returns a future that is NOT bound to the object lifecycle:
EOLIAN static Eina_Future * _my_class_method_creates_a_future(Efl_Object *o, My_Class_Data *pd) { pd->promise = eina_promise_new(efl_loop_future_scheduler_get(efl_loop_get(o)), _promise_cancel, o); return eina_future_new(pd->promise); // unbound, if o dies future remains... OOPS!!! }
Use case: an object creates a promise and returns a future that is bound to the object lifecycle.
EOLIAN static Eina_Future * _my_class_method_creates_a_future(Efl_Object *o, My_Class_Data *pd) { pd->promise = eina_promise_new(efl_loop_future_scheduler_get(efl_loop_get(o)), _promise_cancel, o); return eina_future_then_from_desc(eina_future_new(pd->promise), efl_future_cb(o)); // when used like this, simply binds lifecycle }
Use case: an object creates a promise and returns a future that is bound to the object lifecycle AND checks for resolved type.
EOLIAN static Eina_Future * _my_class_method_creates_a_future(Efl_Object *o, My_Class_Data *pd) { pd->promise = eina_promise_new(efl_loop_future_scheduler_get(efl_loop_get(o)), _promise_cancel, o); return eina_future_then_from_desc(eina_future_new(pd->promise), efl_future_cb(o, .success_type = EINA_VALUE_TYPE_STRING)); // binds lifecycle and check success type is really a string }
Check and return Efl_Object as Eina_Value
Promises resolve passing Eina_Value, these are then forwarded to futures, which can query its contents, pass thru the received value or create a new one to return.
Then EINA_VALUE_TYPE_OBJECT is provided to manage Efl_Object, it will increase reference on eina_value_set() and decrease reference when a value is replaced with another eina_value_set() or when the value is flushed with eina_value_flush(). As done with EINA_VALUE_TYPE_STRING, EINA_VALUE_TYPE_STRINGSHARE and others, eina_value_get() will NOT increment reference, do that yourself.
Eina_Value value; eina_value_setup(&value, EINA_VALUE_TYPE_OBJECT); eina_value_set(&value, o1); // increment o1's reference eina_value_set(&value, o2); // increment o2's reference, decrement o1's reference eina_value_get(&value, &o3); // o3 == o2. No reference is changed eina_value_flush(&value); // decrement o2's reference
Hints for Bindings
Bindings should wrap Eina_Future and Eina_Promise in their native solutions, if any. For example in JavaScript it should interoperate and behave such as JS Promise.
- You must always provide a cancel callback to your promise and cleanup the binding wrapper object.
- In your future callback, convert Eina_Value_Type to a language type, convert EINA_VALUE_TYPE_OBJECT to Eo binding wrappers as well as EINA_VALUE_TYPE_ERROR to language error types/exceptions, such as TypeError in JavaScript or Exception in Python.
- In your future callback, catch language exceptions and convert them to EINA_VALUE_TYPE_ERROR. An user must be able to throw new Error("message") and that will result in EINA_VALUE_TYPE_ERROR being returned. If possible, convert to existing Eina_Error, such as errno.h (in Python OSError can allow you to easily do it), otherwise register your own Eina_Error such as eina_error_msg_register("generic Python exception") or keep a hash language error type -> Eina_Error, if it doesn't exist you register one using its name (recommended).
- Manually expose something like efl_future_from_desc(). Manual bindings should be done to keep it "native" to the target language, then you call efl_future_from_desc() with your own wrapper.
Next steps
- Change Efl.Loop primitives to return an Eina_Future (job, timeout, idler...)
- Change Eolian to produce new futures, including automatic bind to the object
- Allow async/await behavior such as https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function (@barbieri is working on coroutines support)
- Provide helper callback to use GCC/CLang "Blocks" https://en.wikipedia.org/wiki/Blocks_(C_language_extension)
- Integrate a lambda pre-processor, allowing functions to be provided inline and the preprocessor will handle emitting that as its own function and passing that to the future description. (Raster mentioned some existing tool)
- Last Author
- barbieri
- Last Edited
- Oct 10 2017, 11:04 AM
- Projects
- Subscribers
- ProhtMeyhet, barbieri, iscaro