]> git.proxmox.com Git - ceph.git/blob - ceph/src/boost/libs/fiber/doc/integration.qbk
bump version to 12.2.2-pve1
[ceph.git] / ceph / src / boost / libs / fiber / doc / integration.qbk
1 [/
2 Copyright Oliver Kowalke, Nat Goodspeed 2015.
3 Distributed under the Boost Software License, Version 1.0.
4 (See accompanying file LICENSE_1_0.txt or copy at
5 http://www.boost.org/LICENSE_1_0.txt
6 ]
7
8 [/ import path is relative to this .qbk file]
9
10 [#integration]
11 [section:integration Sharing a Thread with Another Main Loop]
12
13 [section Overview]
14
15 As always with cooperative concurrency, it is important not to let any one
16 fiber monopolize the processor too long: that could ["starve] other ready
17 fibers. This section discusses a couple of solutions.
18
19 [endsect]
20 [section Event-Driven Program]
21
22 Consider a classic event-driven program, organized around a main loop that
23 fetches and dispatches incoming I/O events. You are introducing
24 __boost_fiber__ because certain asynchronous I/O sequences are logically
25 sequential, and for those you want to write and maintain code that looks and
26 acts sequential.
27
28 You are launching fibers on the application[s] main thread because certain of
29 their actions will affect its user interface, and the application[s] UI
30 framework permits UI operations only on the main thread. Or perhaps those
31 fibers need access to main-thread data, and it would be too expensive in
32 runtime (or development time) to robustly defend every such data item with
33 thread synchronization primitives.
34
35 You must ensure that the application[s] main loop ['itself] doesn[t] monopolize
36 the processor: that the fibers it launches will get the CPU cycles they need.
37
38 The solution is the same as for any fiber that might claim the CPU for an
39 extended time: introduce calls to [ns_function_link this_fiber..yield]. The
40 most straightforward approach is to call `yield()` on every iteration of your
41 existing main loop. In effect, this unifies the application[s] main loop with
42 __boost_fiber__[s] internal main loop. `yield()` allows the fiber manager to
43 run any fibers that have become ready since the previous iteration of the
44 application[s] main loop. When these fibers have had a turn, control passes to
45 the thread[s] main fiber, which returns from `yield()` and resumes the
46 application[s] main loop.
47
48 [endsect]
49 [#embedded_main_loop]
50 [section Embedded Main Loop]
51
52 More challenging is when the application[s] main loop is embedded in some other
53 library or framework. Such an application will typically, after performing all
54 necessary setup, pass control to some form of `run()` function from which
55 control does not return until application shutdown.
56
57 A __boost_asio__ program might call
58 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service/run.html
59 `io_service::run()`] in this way.
60
61 In general, the trick is to arrange to pass control to [ns_function_link
62 this_fiber..yield] frequently. You could use an
63 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/high_resolution_timer.html
64 Asio timer] for that purpose. You could instantiate the timer, arranging to
65 call a handler function when the timer expires.
66 The handler function could call `yield()`, then reset the timer and arrange to
67 wake up again on its next expiration.
68
69 Since, in this thought experiment, we always pass control to the fiber manager
70 via `yield()`, the calling fiber is never blocked. Therefore there is always
71 at least one ready fiber. Therefore the fiber manager never calls [member_link
72 algorithm..suspend_until].
73
74 Using
75 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service/post.html
76 `io_service::post()`] instead of setting a timer for some nonzero interval
77 would be unfriendly to other threads. When all I/O is pending and all fibers
78 are blocked, the io_service and the fiber manager would simply spin the CPU,
79 passing control back and forth to each other. Using a timer allows tuning the
80 responsiveness of this thread relative to others.
81
82 [endsect]
83 [section Deeper Dive into __boost_asio__]
84
85 By now the alert reader is thinking: but surely, with Asio in particular, we
86 ought to be able to do much better than periodic polling pings!
87
88 [/ @path link is relative to (eventual) doc/html/index.html, hence ../..]
89 This turns out to be surprisingly tricky. We present a possible approach in
90 [@../../examples/asio/round_robin.hpp `examples/asio/round_robin.hpp`].
91
92 [import ../examples/asio/round_robin.hpp]
93 [import ../examples/asio/autoecho.cpp]
94
95 One consequence of using __boost_asio__ is that you must always let Asio
96 suspend the running thread. Since Asio is aware of pending I/O requests, it
97 can arrange to suspend the thread in such a way that the OS will wake it on
98 I/O completion. No one else has sufficient knowledge.
99
100 So the fiber scheduler must depend on Asio for suspension and resumption. It
101 requires Asio handler calls to wake it.
102
103 One dismaying implication is that we cannot support multiple threads calling
104 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service/run.html
105 `io_service::run()`] on the same `io_service` instance. The reason is that
106 Asio provides no way to constrain a particular handler to be called only on a
107 specified thread. A fiber scheduler instance is locked to a particular thread:
108 that instance cannot manage any other thread[s] fibers. Yet if we allow
109 multiple threads to call `io_service::run()` on the same `io_service`
110 instance, a fiber scheduler which needs to sleep can have no guarantee that it
111 will reawaken in a timely manner. It can set an Asio timer, as described above
112 [mdash] but that timer[s] handler may well execute on a different thread!
113
114 Another implication is that since an Asio-aware fiber scheduler (not to
115 mention [link callbacks_asio `boost::fibers::asio::yield`]) depends on handler
116 calls from the `io_service`, it is the application[s] responsibility to ensure
117 that
118 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service/stop.html
119 `io_service::stop()`] is not called until every fiber has terminated.
120
121 It is easier to reason about the behavior of the presented `asio::round_robin`
122 scheduler if we require that after initial setup, the thread[s] main fiber is
123 the fiber that calls `io_service::run()`, so let[s] impose that requirement.
124
125 Naturally, the first thing we must do on each thread using a custom fiber
126 scheduler is call [function_link use_scheduling_algorithm]. However, since
127 `asio::round_robin` requires an `io_service` instance, we must first declare
128 that.
129
130 [asio_rr_setup]
131
132 `use_scheduling_algorithm()` instantiates `asio::round_robin`, which naturally
133 calls its constructor:
134
135 [asio_rr_ctor]
136
137 `asio::round_robin` binds the passed `io_service` reference and initializes a
138 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/steady_timer.html
139 `boost::asio::steady_timer`]:
140
141 [asio_rr_suspend_timer]
142
143 Then it calls
144 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/add_service.html
145 `boost::asio::add_service()`] with a nested `service` struct:
146
147 [asio_rr_service_top]
148 ...
149 [asio_rr_service_bottom]
150
151 The `service` struct has a couple of roles.
152
153 Its foremost role is to manage a
154 [^std::unique_ptr<[@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service__work.html
155 `boost::asio::io_service::work`]>]. We want the `io_service` instance to
156 continue its main loop even when there is no pending Asio I/O.
157
158 But when
159 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service__service/shutdown_service.html
160 `boost::asio::io_service::service::shutdown_service()`] is called, we discard
161 the `io_service::work` instance so the `io_service` can shut down properly.
162
163 Its other purpose is to
164 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service/post.html
165 `post()`] a lambda (not yet shown).
166 Let[s] walk further through the example program before coming back to explain
167 that lambda.
168
169 The `service` constructor returns to `asio::round_robin`[s] constructor,
170 which returns to `use_scheduling_algorithm()`, which returns to the
171 application code.
172
173 Once it has called `use_scheduling_algorithm()`, the application may now
174 launch some number of fibers:
175
176 [asio_rr_launch_fibers]
177
178 Since we don[t] specify a [class_link launch], these fibers are ready
179 to run, but have not yet been entered.
180
181 Having set everything up, the application calls
182 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service/run.html
183 `io_service::run()`]:
184
185 [asio_rr_run]
186
187 Now what?
188
189 Because this `io_service` instance owns an `io_service::work` instance,
190 `run()` does not immediately return. But [mdash] none of the fibers that will
191 perform actual work has even been entered yet!
192
193 Without that initial `post()` call in `service`[s] constructor, ['nothing]
194 would happen. The application would hang right here.
195
196 So, what should the `post()` handler execute? Simply [ns_function_link
197 this_fiber..yield]?
198
199 That would be a promising start. But we have no guarantee that any of the
200 other fibers will initiate any Asio operations to keep the ball rolling. For
201 all we know, every other fiber could reach a similar `this_fiber::yield()`
202 call first. Control would return to the `post()` handler, which would return
203 to Asio, and... the application would hang.
204
205 The `post()` handler could `post()` itself again. But as discussed in [link
206 embedded_main_loop the previous section], once there are actual I/O operations
207 in flight [mdash] once we reach a state in which no fiber is ready [mdash]
208 that would cause the thread to spin.
209
210 We could, of course, set an Asio timer [mdash] again as [link
211 embedded_main_loop previously discussed]. But in this ["deeper dive,] we[,]re
212 trying to do a little better.
213
214 The key to doing better is that since we[,]re in a fiber, we can run an actual
215 loop [mdash] not just a chain of callbacks. We can wait for ["something to
216 happen] by calling
217 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service/run_one.html
218 `io_service::run_one()`] [mdash] or we can execute already-queued Asio
219 handlers by calling
220 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service/poll.html
221 `io_service::poll()`].
222
223 Here[s] the body of the lambda passed to the `post()` call.
224
225 [asio_rr_service_lambda]
226
227 We want this loop to exit once the `io_service` instance has been
228 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service/stopped.html
229 `stopped()`].
230
231 As long as there are ready fibers, we interleave running ready Asio handlers
232 with running ready fibers.
233
234 If there are no ready fibers, we wait by calling `run_one()`. Once any Asio
235 handler has been called [mdash] no matter which [mdash] `run_one()` returns.
236 That handler may have transitioned some fiber to ready state, so we loop back
237 to check again.
238
239 (We won[t] describe `awakened()`, `pick_next()` or `has_ready_fibers()`, as
240 these are just like [member_link round_robin..awakened], [member_link
241 round_robin..pick_next] and [member_link round_robin..has_ready_fibers].)
242
243 That leaves `suspend_until()` and `notify()`.
244
245 Doubtless you have been asking yourself: why are we calling
246 `io_service::run_one()` in the lambda loop? Why not call it in
247 `suspend_until()`, whose very API was designed for just such a purpose?
248
249 Under normal circumstances, when the fiber manager finds no ready fibers, it
250 calls [member_link algorithm..suspend_until]. Why test
251 `has_ready_fibers()` in the lambda loop? Why not leverage the normal
252 mechanism?
253
254 The answer is: it matters who[s] asking.
255
256 Consider the lambda loop shown above. The only __boost_fiber__ APIs it engages
257 are `has_ready_fibers()` and [ns_function_link this_fiber..yield]. `yield()`
258 does not ['block] the calling fiber: the calling fiber does not become
259 unready. It is immediately passed back to [member_link
260 algorithm..awakened], to be resumed in its turn when all other ready
261 fibers have had a chance to run. In other words: during a `yield()` call,
262 ['there is always at least one ready fiber.]
263
264 As long as this lambda loop is still running, the fiber manager does not call
265 `suspend_until()` because it always has a fiber ready to run.
266
267 However, the lambda loop ['itself] can detect the case when no ['other] fibers are
268 ready to run: the running fiber is not ['ready] but ['running.]
269
270 That said, `suspend_until()` and `notify()` are in fact called during orderly
271 shutdown processing, so let[s] try a plausible implementation.
272
273 [asio_rr_suspend_until]
274
275 As you might expect, `suspend_until()` sets an
276 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/steady_timer.html
277 `asio::steady_timer`] to
278 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/basic_waitable_timer/expires_at.html
279 `expires_at()`] the passed
280 [@http://en.cppreference.com/w/cpp/chrono/steady_clock
281 `std::chrono::steady_clock::time_point`]. Usually.
282
283 As indicated in comments, we avoid setting `suspend_timer_` multiple times to
284 the ['same] `time_point` value since every `expires_at()` call cancels any
285 previous
286 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/basic_waitable_timer/async_wait.html
287 `async_wait()`] call. There is a chance that we could spin. Reaching
288 `suspend_until()` means the fiber manager intends to yield the processor to
289 Asio. Cancelling the previous `async_wait()` call would fire its handler,
290 causing `run_one()` to return, potentially causing the fiber manager to call
291 `suspend_until()` again with the same `time_point` value...
292
293 Given that we suspend the thread by calling `io_service::run_one()`, what[s]
294 important is that our `async_wait()` call will cause a handler to run, which
295 will cause `run_one()` to return. It[s] not so important specifically what
296 that handler does.
297
298 [asio_rr_notify]
299
300 Since an `expires_at()` call cancels any previous `async_wait()` call, we can
301 make `notify()` simply call `steady_timer::expires_at()`. That should cause
302 the `io_service` to call the `async_wait()` handler with `operation_aborted`.
303
304 The comments in `notify()` explain why we call `expires_at()` rather than
305 [@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/basic_waitable_timer/cancel.html
306 `cancel()`].
307
308 [/ @path link is relative to (eventual) doc/html/index.html, hence ../..]
309 This `boost::fibers::asio::round_robin` implementation is used in
310 [@../../examples/asio/autoecho.cpp `examples/asio/autoecho.cpp`].
311
312 It seems possible that you could put together a more elegant Fiber / Asio
313 integration. But as noted at the outset: it[s] tricky.
314
315 [endsect]
316 [endsect]