Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

[COMMIT seastar master] Merge 'Update tutorial.md to reflect updated preemption methods' from Travis Downs

6 views
Skip to first unread message

Commit Bot

<bot@cloudius-systems.com>
unread,
Jun 13, 2024, 3:29:24 AM6/13/24
to seastar-dev@googlegroups.com, Nadav Har'El
From: Nadav Har'El <n...@scylladb.com>
Committer: Nadav Har'El <n...@scylladb.com>
Branch: master

Merge 'Update tutorial.md to reflect updated preemption methods' from Travis Downs

I was actually surprised to see that `then()` no longer does a preemption check (and this certainly leads to better code generation!).

I checked the tutorial and notice that it refers a different mechanism entirely, a 256-continuation counter but I can't find any trace of this in the code, so I assume it preceeded the time-based preemption check which has subsequently been removed.

So:

- Rip out mention of the 256-counter
- Talk about preemption which isn't really covered in the tutorial and add that then() is not a suspension point (but co_await is, by default)

Closes #2292

* github.com:scylladb/seastar:
tutorial.md: fix typos
Update tutorial.md to reflect update preemption methods
tutorial.md: remove trailing whitespace

---
diff --git a/doc/tutorial.md b/doc/tutorial.md
--- a/doc/tutorial.md
+++ b/doc/tutorial.md
@@ -188,7 +188,7 @@ Couldn't start application: std::runtime_error (insufficient physical memory)
```

# Introducing futures and continuations
-Futures and continuations, which we will introduce now, are the building blocks of asynchronous programming in Seastar. Their strength lies in the ease of composing them together into a large, complex, asynchronous program, while keeping the code fairly readable and understandable.
+Futures and continuations, which we will introduce now, are the building blocks of asynchronous programming in Seastar. Their strength lies in the ease of composing them together into a large, complex, asynchronous program, while keeping the code fairly readable and understandable.

A [future](\ref future) is a result of a computation that may not be available yet.
Examples include:
@@ -316,9 +316,7 @@ The function `slow()` deserves more explanation. As usual, this function returns
This example begins to show the convenience of the futures programming model, which allows the programmer to neatly encapsulate complex asynchronous operations. `slow()` might involve a complex asynchronous operation requiring multiple steps, but its user can use it just as easily as a simple `sleep()`, and Seastar's engine takes care of running the continuations whose futures have become ready at the right time.

## Ready futures
-A future value might already be ready when `then()` is called to chain a continuation to it. This important case is optimized, and *usually* the continuation is run immediately instead of being registered to run later in the next iteration of the event loop.
-
-This optimization is done *usually*, though sometimes it is avoided: The implementation of `then()` holds a counter of such immediate continuations, and after many continuations have been run immediately without returning to the event loop (currently the limit is 256), the next continuation is deferred to the event loop in any case. This is important because in some cases (such as future loops, discussed later) we could find that each ready continuation spawns a new one, and without this limit we can starve the event loop. It is important not to starve the event loop, as this would starve continuations of futures that weren't ready but have since become ready, and also starve the important **polling** done by the event loop (e.g., checking whether there is new activity on the network card).
+A future value might already be ready when `then()` is called to chain a continuation to it. This important case is optimized, and the continuation is run immediately instead of being registered to run later in the next iteration of the event loop.

`make_ready_future<>` can be used to return a future which is already ready. The following example is identical to the previous one, except the promise function `fast()` returns a future which is already ready, and not one which will be ready in a second as in the previous example. The nice thing is that the consumer of the future does not care, and uses the future in the same way in both cases.

@@ -337,6 +335,16 @@ seastar::future<> f() {
}
```

+## Preemption and Task Quota
+
+As described above, an existing fiber of execution will yield back to the event loop when it performs a blocking operation such as IO or sleeping, as it has no more work to do until this blocking operation completes. Should a fiber have a lot of CPU bound work to do without any intervening blocking operations, however, it is important that execution is still yielded back to the event loop periodically.
+
+This is implemented via _preemption_: which can only occur at specific preemption points. At these points the fiber's remaining _task quota_ is checked and it has been exceeded the fiber yields. The task quota is a measure of how long tasks should be allowed to run before yielding to the event loop, and is set to 500 µs by default.
+
+It is important not to starve the event loop, as this would starve continuations of futures that weren't ready but have since become ready, and also starve the important **polling** done by the event loop (e.g., checking whether there is new activity on the network card). For example, iterating over a large container while doing CPU-bound work without any suspension points could starve the reactor and cause a _reactor stall_, which refers to a substantial period of time (e.g., more than 20 milliseconds) during which a task does not yield.
+
+Many seastar constructs such as looping constructs have built-in preemption points. You may also insert your own preemption points by calling `seastar::maybe_yield`, which performs a preemption check. Coroutines will also perform a preemption check at each `co_await`. Note that there is _not_ a preemption check between continuations attached to a future with `then()`, so a recursive future loop without explicit preemption checks may starve the reactor.
+
# Coroutines

Note: coroutines require C++20 and a supporting compiler. Clang 10 and above is known to work.
@@ -412,9 +420,9 @@ another asynchronously. From the consumer of the view's perspective, it can retr
the return value of the coroutine. From the coroutine's perspective, it is able to produce the elements multiple times
using `co_yield` without "leaving" the coroutine. A function producing a sequence of values can be named "generator".
But unlike the regular coroutine which returns a single `seastar::future<T>`, a generator should return
-`seastar::coroutine::experimental::generator<T, Container>`. Where `T` is the type of the elements, while `Container`
-is a template, which is used to store the elements. Because, underneath of Seastar's generator implementation, a
-bounded buffer is used for holding the elements not yet retrieved by the consumer, there is a design decision to make --
+`seastar::coroutine::experimental::generator<T, Container>`. Where `T` is the type of the elements, while `Container`
+is a template, which is used to store the elements. Because, underneath of Seastar's generator implementation, a
+bounded buffer is used for holding the elements not yet retrieved by the consumer, there is a design decision to make --
what kind of container should be used, and what its maximum size should be. To define the bounded buffer, developers
need to:

@@ -424,8 +432,8 @@ need to:

But there is an exception, if the buffer's size is one, we assume that the programmer is likely to use `std::optional`
for the bounded buffer, so it's not required to pass the maximum size of the buffer as the first parameter in this case.
-But if a coroutine uses `std::optional` as its buffer, and its function sigature still lists the size as its first
-parameter, it will not break anything. As this parameter will just be ignored by the underlying implementation.
+But if a coroutine uses `std::optional` as its buffer, and its function signature still lists the size as its first
+parameter, it will not break anything. As this parameter will just be ignored by the underlying implementation.

Following is an example

@@ -467,10 +475,10 @@ is too slow so that there are `max_dishes_on_table` dishes left on the table, th
until the number of dishes is less than this setting. Please note, as explained above, despite that this
parameter is not referenced by the coroutine's body, it is actually passed to the generator's promise
constructor, which in turn creates the buffer, as we are not using `std::optional` here. On the other hand,
-apparently, if there is no dishes on the table, the diner would wait for new ones to be prepared by the chef.
+apparently, if there is no dishes on the table, the diner would wait for new ones to be prepared by the chef.

Please note, `generator<T, Container>` is still at its early stage of developing,
-the public interface this template is subject to change before it is stablized enough.
+the public interface this template is subject to change before it is stabilized enough.

## Exceptions in coroutines

@@ -652,7 +660,7 @@ Using capture-by-*move* in continuations is also very useful in Seastar applicat
int do_something(std::unique_ptr<T> obj) {
// do some computation based on the contents of obj, let's say the result is 17
return 17;
- // at this point, obj goes out of scope so the compiler delete()s it.
+ // at this point, obj goes out of scope so the compiler delete()s it.
```
By using unique_ptr in this way, the caller passes an object to the function, but tells it the object is now its exclusive responsibility - and when the function is done with the object, it automatically deletes it. How do we use unique_ptr in a continuation? The following won't work:

@@ -735,7 +743,7 @@ Usually, aborting the current chain of operations and returning an exception is
1. `.then_wrapped()`: instead of passing the values carried by the future into the continuation, `.then_wrapped()` passes the input future to the continuation. The future is guaranteed to be in ready state, so the continuation can examine whether it contains a value or an exception, and take appropriate action.
2. `.finally()`: similar to a Java finally block, a `.finally()` continuation is executed whether or not its input future carries an exception or not. The result of the finally continuation is its input future, so `.finally()` can be used to insert code in a flow that is executed unconditionally, but otherwise does not alter the flow.

-The following example illustates usage of `then_wrapped` and `finally`:
+The following example illustrates usage of `then_wrapped` and `finally`:

```cpp
#include <seastar/core/future.hh>
@@ -976,7 +984,7 @@ seastar::future<> f() {
}
}
```
-`do_with` will *do* the given function *with* the given object alive.
+`do_with` will *do* the given function *with* the given object alive.

`do_with` saves the given object on the heap, and calls the given lambda with a reference to the new object. Finally it ensures that the new object is destroyed after the returned future is resolved. Usually, do_with is given an *rvalue*, i.e., an unnamed temporary object or an `std::move()`ed object, and `do_with` moves that object into its final place on the heap. `do_with` returns a future which resolves after everything described above is done (the lambda's future is resolved and the object is destroyed).

@@ -1069,12 +1077,12 @@ TODO: Talk about if we have a `future<int>` variable, as soon as we `get()` or `
# Fibers
Seastar continuations are normally short, but often chained to one another, so that one continuation does a bit of work and then schedules another continuation for later. Such chains can be long, and often even involve loopings - see the following section, "Loops". We call such chains "fibers" of execution.

-These fibers are not threads - each is just a string of continuations - but they share some common requirements with traditional threads. For example, we want to avoid one fiber getting starved while a second fiber continuously runs its continuations one after another. As another example, fibers may want to communicate - e.g., one fiber produces data that a second fiber consumes, and we wish to ensure that both fibers get a chance to run, and that if one stops prematurely, the other doesn't hang forever.
+These fibers are not threads - each is just a string of continuations - but they share some common requirements with traditional threads. For example, we want to avoid one fiber getting starved while a second fiber continuously runs its continuations one after another. As another example, fibers may want to communicate - e.g., one fiber produces data that a second fiber consumes, and we wish to ensure that both fibers get a chance to run, and that if one stops prematurely, the other doesn't hang forever.

TODO: Mention fiber-related sections like loops, semaphores, gates, pipes, etc.

# Loops
-A majority of time-consuming computations involve using loops. Seastar provides several primitives for expressing them in a way that composes nicely with the future/promise model. A very important aspect of Seastar loop primitives is that each iteration is followed by a preemption point, thus allowing other tasks to run inbetween iterations.
+A majority of time-consuming computations involve using loops. Seastar provides several primitives for expressing them in a way that composes nicely with the future/promise model. A very important aspect of Seastar loop primitives is that each iteration is followed by a preemption point, thus allowing other tasks to run in between iterations.

## repeat
A loop created with `repeat` executes its body until it receives a `stop_iteration` object, which informs if the iteration should continue (`stop_iteration::no`) or stop (`stop_iteration::yes`). Next iteration will be launched only after the first one has finished. The loop body passed to `repeat` is expected to have a `future<stop_iteration>` return type.
@@ -1187,7 +1195,7 @@ The first variant of `when_all()` is variadic, i.e., the futures are given as se
future<> f() {
using namespace std::chrono_literals;
future<int> slow_two = sleep(2s).then([] { return 2; });
- return when_all(sleep(1s), std::move(slow_two),
+ return when_all(sleep(1s), std::move(slow_two),
make_ready_future<double>(3.5)
).discard_result();
}
@@ -1233,7 +1241,7 @@ future<> f() {

Both futures are `available()` (resolved), but the second has `failed()` (resulted in an exception instead of a value). Note how we called `ignore_ready_future()` on this failed future, because silently ignoring a failed future is considered a bug, and will result in an "Exceptional future ignored" error message. More typically, an application will log the failed future instead of ignoring it.

-The above example demonstrate that `when_all()` is inconvenient and verbose to use properly. The results are wrapped in a tuple, leading to verbose tuple syntax, and uses ready futures which must all be inspected individually for an exception to avoid error messages.
+The above example demonstrate that `when_all()` is inconvenient and verbose to use properly. The results are wrapped in a tuple, leading to verbose tuple syntax, and uses ready futures which must all be inspected individually for an exception to avoid error messages.

So Seastar also provides an easier to use `when_all_succeed()` function. This function too returns a future which resolves when all the given futures have resolved. If all of them succeeded, it passes a tuple of the resulting values to continuation, without wrapping each of them in a future first. Sometimes, it could be tedious to unpack the tuple for consuming the resulting values. In that case, `then_unpack()` can be used in place of `then()`. `then_unpack()` unpacks the returned tuple and passes its elements to the following continuation as its parameters. If, however, one or more of the futures failed, `when_all_succeed()` resolves to a failed future, containing the exception from one of the failed futures. If more than one of the given future failed, one of those will be passed on (it is unspecified which one is chosen), and the rest will be silently ignored. For example,

@@ -1349,7 +1357,7 @@ seastar::future<> g() {

Note the somewhat convoluted way that `get_units()` needs to be used: The continuations must be nested because we need the `units` object to be moved to the last continuation. If `slow()` returns a future (and does not throw immediately), the `finally()` continuation captures the `units` object until everything is done, but does not run any code.

-Seastars programmers should generally avoid using the the `semaphore::wait()` and `semaphore::signal()` functions directly, and always prefer either `with_semaphore()` (when applicable) or `get_units()`.
+Seastar's programmers should generally avoid using the the `semaphore::wait()` and `semaphore::signal()` functions directly, and always prefer either `with_semaphore()` (when applicable) or `get_units()`.


## Limiting resource use
@@ -1407,7 +1415,7 @@ seastar::future<> f() {
return seastar::repeat([&limit] {
return limit.wait(1).then([&limit] {
seastar::futurize_invoke(slow).finally([&limit] {
- limit.signal(1);
+ limit.signal(1);
});
return seastar::stop_iteration::no;
});
@@ -1844,7 +1852,7 @@ return s.invoke_on(0, [] (my_service& local_service) {
});
```

-This runs the lambda function on shard 0, with a reference to the local `my_service` object on that shard.
+This runs the lambda function on shard 0, with a reference to the local `my_service` object on that shard.


# Shutting down cleanly
@@ -2052,7 +2060,7 @@ seastar::future_state<>::~future_state() at include/seastar/core/future.hh:414
f() at test.cc:12
```

-Here we see that the warning message was printed by the `seastar::report_failed_future()` function which was called when destroying a future (`future<>::~future`) that had not been handled. The future's destructor was called in line 11 of our test code (`26.cc`), which is indeed the line where we called `g()` and ignored its result.
+Here we see that the warning message was printed by the `seastar::report_failed_future()` function which was called when destroying a future (`future<>::~future`) that had not been handled. The future's destructor was called in line 11 of our test code (`26.cc`), which is indeed the line where we called `g()` and ignored its result.
This backtrace gives us an accurate understanding of where our code destroyed an exceptional future without handling it first, which is usually helpful in solving these kinds of bugs. Note that this technique does not tell us where the exception was first created, nor what code passed around the exceptional future before it was destroyed - we just learn where the future was destroyed. To learn where the exception was originally thrown, see the next section:

## Finding where an exception was thrown
Reply all
Reply to author
Forward
0 new messages