integration.qbk 14 KB

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