]>
Commit | Line | Data |
---|---|---|
7c673cae FG |
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] |