Line data Source code
1 : //
2 : // Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com)
3 : //
4 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 : //
7 : // Official repository: https://github.com/cppalliance/corosio
8 : //
9 :
10 : #ifndef BOOST_COROSIO_TCP_SERVER_HPP
11 : #define BOOST_COROSIO_TCP_SERVER_HPP
12 :
13 : #include <boost/corosio/detail/config.hpp>
14 : #include <boost/corosio/detail/except.hpp>
15 : #include <boost/corosio/tcp_acceptor.hpp>
16 : #include <boost/corosio/tcp_socket.hpp>
17 : #include <boost/corosio/io_context.hpp>
18 : #include <boost/corosio/endpoint.hpp>
19 : #include <boost/capy/task.hpp>
20 : #include <boost/capy/concept/execution_context.hpp>
21 : #include <boost/capy/concept/io_awaitable.hpp>
22 : #include <boost/capy/concept/executor.hpp>
23 : #include <boost/capy/ex/any_executor.hpp>
24 : #include <boost/capy/ex/run_async.hpp>
25 :
26 : #include <coroutine>
27 : #include <memory>
28 : #include <ranges>
29 : #include <vector>
30 :
31 : namespace boost::corosio {
32 :
33 : #ifdef _MSC_VER
34 : #pragma warning(push)
35 : #pragma warning(disable: 4251) // class needs to have dll-interface
36 : #endif
37 :
38 : /** TCP server with pooled workers.
39 :
40 : This class manages a pool of reusable worker objects that handle
41 : incoming connections. When a connection arrives, an idle worker
42 : is dispatched to handle it. After the connection completes, the
43 : worker returns to the pool for reuse, avoiding allocation overhead
44 : per connection.
45 :
46 : Workers are set via @ref set_workers as a forward range of
47 : pointer-like objects (e.g., `unique_ptr<worker_base>`). The server
48 : takes ownership of the container via type erasure.
49 :
50 : @par Thread Safety
51 : Distinct objects: Safe.
52 : Shared objects: Unsafe.
53 :
54 : @par Lifecycle
55 : The server operates in three states:
56 :
57 : - **Stopped**: Initial state, or after @ref join completes.
58 : - **Running**: After @ref start, actively accepting connections.
59 : - **Stopping**: After @ref stop, draining active work.
60 :
61 : State transitions:
62 : @code
63 : [Stopped] --start()--> [Running] --stop()--> [Stopping] --join()--> [Stopped]
64 : @endcode
65 :
66 : @par Running the Server
67 : @code
68 : io_context ioc;
69 : tcp_server srv(ioc, ioc.get_executor());
70 : srv.set_workers(make_workers(ioc, 100));
71 : srv.bind(endpoint{address_v4::any(), 8080});
72 : srv.start();
73 : ioc.run(); // Blocks until all work completes
74 : @endcode
75 :
76 : @par Graceful Shutdown
77 : To shut down gracefully, call @ref stop then drain the io_context:
78 : @code
79 : // From a signal handler or timer callback:
80 : srv.stop();
81 :
82 : // ioc.run() returns after pending work drains.
83 : // Then from the thread that called ioc.run():
84 : srv.join(); // Wait for accept loops to finish
85 : @endcode
86 :
87 : @par Restart After Stop
88 : The server can be restarted after a complete shutdown cycle.
89 : You must drain the io_context and call @ref join before restarting:
90 : @code
91 : srv.start();
92 : ioc.run_for( 10s ); // Run for a while
93 : srv.stop(); // Signal shutdown
94 : ioc.run(); // REQUIRED: drain pending completions
95 : srv.join(); // REQUIRED: wait for accept loops
96 :
97 : // Now safe to restart
98 : srv.start();
99 : ioc.run();
100 : @endcode
101 :
102 : @par WARNING: What NOT to Do
103 : - Do NOT call @ref join from inside a worker coroutine (deadlock).
104 : - Do NOT call @ref join from a thread running `ioc.run()` (deadlock).
105 : - Do NOT call @ref start without completing @ref join after @ref stop.
106 : - Do NOT call `ioc.stop()` for graceful shutdown; use @ref stop instead.
107 :
108 : @par Example
109 : @code
110 : class my_worker : public tcp_server::worker_base
111 : {
112 : corosio::tcp_socket sock_;
113 : capy::any_executor ex_;
114 : public:
115 : my_worker(io_context& ctx)
116 : : sock_(ctx)
117 : , ex_(ctx.get_executor())
118 : {
119 : }
120 :
121 : corosio::tcp_socket& socket() override { return sock_; }
122 :
123 : void run(launcher launch) override
124 : {
125 : launch(ex_, [](corosio::tcp_socket* sock) -> capy::task<>
126 : {
127 : // handle connection using sock
128 : co_return;
129 : }(&sock_));
130 : }
131 : };
132 :
133 : auto make_workers(io_context& ctx, int n)
134 : {
135 : std::vector<std::unique_ptr<tcp_server::worker_base>> v;
136 : v.reserve(n);
137 : for(int i = 0; i < n; ++i)
138 : v.push_back(std::make_unique<my_worker>(ctx));
139 : return v;
140 : }
141 :
142 : io_context ioc;
143 : tcp_server srv(ioc, ioc.get_executor());
144 : srv.set_workers(make_workers(ioc, 100));
145 : @endcode
146 :
147 : @see worker_base, set_workers, launcher
148 : */
149 : class BOOST_COROSIO_DECL
150 : tcp_server
151 : {
152 : public:
153 : class worker_base; ///< Abstract base for connection handlers.
154 : class launcher; ///< Move-only handle to launch worker coroutines.
155 :
156 : private:
157 : struct waiter
158 : {
159 : waiter* next;
160 : std::coroutine_handle<> h;
161 : worker_base* w;
162 : };
163 :
164 : struct impl;
165 :
166 : static impl* make_impl(capy::execution_context& ctx);
167 :
168 : impl* impl_;
169 : capy::any_executor ex_;
170 : waiter* waiters_ = nullptr;
171 : worker_base* idle_head_ = nullptr; // Forward list: available workers
172 : worker_base* active_head_ = nullptr; // Doubly linked: workers handling connections
173 : worker_base* active_tail_ = nullptr; // Tail for O(1) push_back
174 : std::size_t active_accepts_ = 0; // Number of active do_accept coroutines
175 : std::shared_ptr<void> storage_; // Owns the worker container (type-erased)
176 : bool running_ = false;
177 :
178 : // Idle list (forward/singly linked) - push front, pop front
179 45 : void idle_push(worker_base* w) noexcept
180 : {
181 45 : w->next_ = idle_head_;
182 45 : idle_head_ = w;
183 45 : }
184 :
185 9 : worker_base* idle_pop() noexcept
186 : {
187 9 : auto* w = idle_head_;
188 9 : if(w) idle_head_ = w->next_;
189 9 : return w;
190 : }
191 :
192 9 : bool idle_empty() const noexcept { return idle_head_ == nullptr; }
193 :
194 : // Active list (doubly linked) - push back, remove anywhere
195 3 : void active_push(worker_base* w) noexcept
196 : {
197 3 : w->next_ = nullptr;
198 3 : w->prev_ = active_tail_;
199 3 : if(active_tail_)
200 0 : active_tail_->next_ = w;
201 : else
202 3 : active_head_ = w;
203 3 : active_tail_ = w;
204 3 : }
205 :
206 9 : void active_remove(worker_base* w) noexcept
207 : {
208 : // Skip if not in active list (e.g., after failed accept)
209 9 : if(w != active_head_ && w->prev_ == nullptr)
210 6 : return;
211 3 : if(w->prev_)
212 0 : w->prev_->next_ = w->next_;
213 : else
214 3 : active_head_ = w->next_;
215 3 : if(w->next_)
216 0 : w->next_->prev_ = w->prev_;
217 : else
218 3 : active_tail_ = w->prev_;
219 3 : w->prev_ = nullptr; // Mark as not in active list
220 : }
221 :
222 : template<capy::Executor Ex>
223 : struct launch_wrapper
224 : {
225 : struct promise_type
226 : {
227 : Ex ex; // Stored directly in frame, no allocation
228 : std::stop_token st;
229 :
230 : // For regular coroutines: first arg is executor, second is stop token
231 : template<class E, class S, class... Args>
232 : requires capy::Executor<std::decay_t<E>>
233 : promise_type(E e, S s, Args&&...)
234 : : ex(std::move(e))
235 : , st(std::move(s))
236 : {
237 : }
238 :
239 : // For lambda coroutines: first arg is closure, second is executor, third is stop token
240 : template<class Closure, class E, class S, class... Args>
241 : requires (!capy::Executor<std::decay_t<Closure>> &&
242 : capy::Executor<std::decay_t<E>>)
243 3 : promise_type(Closure&&, E e, S s, Args&&...)
244 3 : : ex(std::move(e))
245 3 : , st(std::move(s))
246 : {
247 3 : }
248 :
249 3 : launch_wrapper get_return_object() noexcept {
250 3 : return {std::coroutine_handle<promise_type>::from_promise(*this)};
251 : }
252 3 : std::suspend_always initial_suspend() noexcept { return {}; }
253 3 : std::suspend_never final_suspend() noexcept { return {}; }
254 3 : void return_void() noexcept {}
255 0 : void unhandled_exception() { std::terminate(); }
256 :
257 : // Pass through simple awaitables, inject executor/stop_token for IoAwaitable
258 : template<class Awaitable>
259 6 : auto await_transform(Awaitable&& a)
260 : {
261 : using AwaitableT = std::decay_t<Awaitable>;
262 : // Simple awaitable: has await_suspend(coroutine_handle<>) but not IoAwaitable
263 : if constexpr (
264 : requires { a.await_suspend(std::declval<std::coroutine_handle<>>()); } &&
265 : !capy::IoAwaitable<AwaitableT>)
266 : {
267 : return std::forward<Awaitable>(a);
268 : }
269 : else
270 : {
271 : struct adapter
272 : {
273 : AwaitableT aw;
274 : Ex* ex_ptr;
275 : std::stop_token* st_ptr;
276 :
277 6 : bool await_ready() { return aw.await_ready(); }
278 6 : decltype(auto) await_resume() { return aw.await_resume(); }
279 :
280 6 : auto await_suspend(std::coroutine_handle<promise_type> h)
281 : {
282 : static_assert(capy::IoAwaitable<AwaitableT>);
283 6 : return aw.await_suspend(h, *ex_ptr, *st_ptr);
284 : }
285 : };
286 9 : return adapter{std::forward<Awaitable>(a), &ex, &st};
287 : }
288 3 : }
289 : };
290 :
291 : std::coroutine_handle<promise_type> h;
292 :
293 3 : launch_wrapper(std::coroutine_handle<promise_type> handle) noexcept
294 3 : : h(handle)
295 : {
296 3 : }
297 :
298 3 : ~launch_wrapper()
299 : {
300 3 : if(h)
301 0 : h.destroy();
302 3 : }
303 :
304 : launch_wrapper(launch_wrapper&& o) noexcept
305 : : h(std::exchange(o.h, nullptr))
306 : {
307 : }
308 :
309 : launch_wrapper(launch_wrapper const&) = delete;
310 : launch_wrapper& operator=(launch_wrapper const&) = delete;
311 : launch_wrapper& operator=(launch_wrapper&&) = delete;
312 : };
313 :
314 : // Named functor to avoid incomplete lambda type in coroutine promise
315 : template<class Executor>
316 : struct launch_coro
317 : {
318 3 : launch_wrapper<Executor> operator()(
319 : Executor,
320 : std::stop_token,
321 : tcp_server* self,
322 : capy::task<void> t,
323 : worker_base* wp)
324 : {
325 : // Executor and stop token stored in promise via constructor
326 : co_await std::move(t);
327 : co_await self->push(*wp);
328 6 : }
329 : };
330 :
331 : class push_awaitable
332 : {
333 : tcp_server& self_;
334 : worker_base& w_;
335 :
336 : public:
337 9 : push_awaitable(
338 : tcp_server& self,
339 : worker_base& w) noexcept
340 9 : : self_(self)
341 9 : , w_(w)
342 : {
343 9 : }
344 :
345 9 : bool await_ready() const noexcept
346 : {
347 9 : return false;
348 : }
349 :
350 : template<class Ex>
351 : std::coroutine_handle<>
352 9 : await_suspend(
353 : std::coroutine_handle<> h,
354 : Ex const&, std::stop_token) noexcept
355 : {
356 : // Dispatch to server's executor before touching shared state
357 9 : self_.ex_.dispatch(h);
358 9 : return std::noop_coroutine();
359 : }
360 :
361 9 : void await_resume() noexcept
362 : {
363 : // Running on server executor - safe to modify lists
364 : // Remove from active (if present), then wake waiter or add to idle
365 9 : self_.active_remove(&w_);
366 9 : if(self_.waiters_)
367 : {
368 0 : auto* wait = self_.waiters_;
369 0 : self_.waiters_ = wait->next;
370 0 : wait->w = &w_;
371 0 : self_.ex_.post(wait->h);
372 : }
373 : else
374 : {
375 9 : self_.idle_push(&w_);
376 : }
377 9 : }
378 : };
379 :
380 : class pop_awaitable
381 : {
382 : tcp_server& self_;
383 : waiter wait_;
384 :
385 : public:
386 9 : pop_awaitable(tcp_server& self) noexcept
387 9 : : self_(self)
388 9 : , wait_{}
389 : {
390 9 : }
391 :
392 9 : bool await_ready() const noexcept
393 : {
394 9 : return !self_.idle_empty();
395 : }
396 :
397 : template<class Ex>
398 : bool
399 0 : await_suspend(
400 : std::coroutine_handle<> h,
401 : Ex const&, std::stop_token) noexcept
402 : {
403 : // Running on server executor (do_accept runs there)
404 0 : wait_.h = h;
405 0 : wait_.w = nullptr;
406 0 : wait_.next = self_.waiters_;
407 0 : self_.waiters_ = &wait_;
408 0 : return true;
409 : }
410 :
411 9 : worker_base& await_resume() noexcept
412 : {
413 : // Running on server executor
414 9 : if(wait_.w)
415 0 : return *wait_.w; // Woken by push_awaitable
416 9 : return *self_.idle_pop();
417 : }
418 : };
419 :
420 9 : push_awaitable push(worker_base& w)
421 : {
422 9 : return push_awaitable{*this, w};
423 : }
424 :
425 : // Synchronous version for destructor/guard paths
426 : // Must be called from server executor context
427 0 : void push_sync(worker_base& w) noexcept
428 : {
429 0 : active_remove(&w);
430 0 : if(waiters_)
431 : {
432 0 : auto* wait = waiters_;
433 0 : waiters_ = wait->next;
434 0 : wait->w = &w;
435 0 : ex_.post(wait->h);
436 : }
437 : else
438 : {
439 0 : idle_push(&w);
440 : }
441 0 : }
442 :
443 9 : pop_awaitable pop()
444 : {
445 9 : return pop_awaitable{*this};
446 : }
447 :
448 : capy::task<void> do_accept(tcp_acceptor& acc);
449 :
450 : public:
451 : /** Abstract base class for connection handlers.
452 :
453 : Derive from this class to implement custom connection handling.
454 : Each worker owns a socket and is reused across multiple
455 : connections to avoid per-connection allocation.
456 :
457 : @see tcp_server, launcher
458 : */
459 : class BOOST_COROSIO_DECL
460 : worker_base
461 : {
462 : // Ordered largest to smallest for optimal packing
463 : std::stop_source stop_; // ~16 bytes
464 : worker_base* next_ = nullptr; // 8 bytes - used by idle and active lists
465 : worker_base* prev_ = nullptr; // 8 bytes - used only by active list
466 :
467 : friend class tcp_server;
468 :
469 : public:
470 : /// Destroy the worker.
471 36 : virtual ~worker_base() = default;
472 :
473 : /** Handle an accepted connection.
474 :
475 : Called when this worker is dispatched to handle a new
476 : connection. The implementation must invoke the launcher
477 : exactly once to start the handling coroutine.
478 :
479 : @param launch Handle to launch the connection coroutine.
480 : */
481 : virtual void run(launcher launch) = 0;
482 :
483 : /// Return the socket used for connections.
484 : virtual corosio::tcp_socket& socket() = 0;
485 : };
486 :
487 : /** Move-only handle to launch a worker coroutine.
488 :
489 : Passed to @ref worker_base::run to start the connection-handling
490 : coroutine. The launcher ensures the worker returns to the idle
491 : pool when the coroutine completes or if launching fails.
492 :
493 : The launcher must be invoked exactly once via `operator()`.
494 : If destroyed without invoking, the worker is returned to the
495 : idle pool automatically.
496 :
497 : @see worker_base::run
498 : */
499 : class BOOST_COROSIO_DECL
500 : launcher
501 : {
502 : tcp_server* srv_;
503 : worker_base* w_;
504 :
505 : friend class tcp_server;
506 :
507 3 : launcher(tcp_server& srv, worker_base& w) noexcept
508 3 : : srv_(&srv)
509 3 : , w_(&w)
510 : {
511 3 : }
512 :
513 : public:
514 : /// Return the worker to the pool if not launched.
515 3 : ~launcher()
516 : {
517 3 : if(w_)
518 0 : srv_->push_sync(*w_);
519 3 : }
520 :
521 : launcher(launcher&& o) noexcept
522 : : srv_(o.srv_)
523 : , w_(std::exchange(o.w_, nullptr))
524 : {
525 : }
526 : launcher(launcher const&) = delete;
527 : launcher& operator=(launcher const&) = delete;
528 : launcher& operator=(launcher&&) = delete;
529 :
530 : /** Launch the connection-handling coroutine.
531 :
532 : Starts the given coroutine on the specified executor. When
533 : the coroutine completes, the worker is automatically returned
534 : to the idle pool.
535 :
536 : @param ex The executor to run the coroutine on.
537 : @param task The coroutine to execute.
538 :
539 : @throws std::logic_error If this launcher was already invoked.
540 : */
541 : template<class Executor>
542 3 : void operator()(Executor const& ex, capy::task<void> task)
543 : {
544 3 : if(! w_)
545 0 : detail::throw_logic_error(); // launcher already invoked
546 :
547 3 : auto* w = std::exchange(w_, nullptr);
548 :
549 : // Worker is being dispatched - add to active list
550 3 : srv_->active_push(w);
551 :
552 : // Return worker to pool if coroutine setup throws
553 : struct guard_t {
554 : tcp_server* srv;
555 : worker_base* w;
556 3 : ~guard_t() { if(w) srv->push_sync(*w); }
557 3 : } guard{srv_, w};
558 :
559 : // Reset worker's stop source for this connection
560 3 : w->stop_ = {};
561 3 : auto st = w->stop_.get_token();
562 :
563 3 : auto wrapper = launch_coro<Executor>{}(
564 3 : ex, st, srv_, std::move(task), w);
565 :
566 : // Executor and stop token stored in promise via constructor
567 3 : ex.post(std::exchange(wrapper.h, nullptr)); // Release before post
568 3 : guard.w = nullptr; // Success - dismiss guard
569 3 : }
570 : };
571 :
572 : /** Construct a TCP server.
573 :
574 : @tparam Ctx Execution context type satisfying ExecutionContext.
575 : @tparam Ex Executor type satisfying Executor.
576 :
577 : @param ctx The execution context for socket operations.
578 : @param ex The executor for dispatching coroutines.
579 :
580 : @par Example
581 : @code
582 : tcp_server srv(ctx, ctx.get_executor());
583 : srv.set_workers(make_workers(ctx, 100));
584 : srv.bind(endpoint{...});
585 : srv.start();
586 : @endcode
587 : */
588 : template<
589 : capy::ExecutionContext Ctx,
590 : capy::Executor Ex>
591 9 : tcp_server(Ctx& ctx, Ex ex)
592 9 : : impl_(make_impl(ctx))
593 9 : , ex_(std::move(ex))
594 : {
595 9 : }
596 :
597 : public:
598 : ~tcp_server();
599 : tcp_server(tcp_server const&) = delete;
600 : tcp_server& operator=(tcp_server const&) = delete;
601 : tcp_server(tcp_server&& o) noexcept;
602 : tcp_server& operator=(tcp_server&& o) noexcept;
603 :
604 : /** Bind to a local endpoint.
605 :
606 : Creates an acceptor listening on the specified endpoint.
607 : Multiple endpoints can be bound by calling this method
608 : multiple times before @ref start.
609 :
610 : @param ep The local endpoint to bind to.
611 :
612 : @return The error code if binding fails.
613 : */
614 : std::error_code
615 : bind(endpoint ep);
616 :
617 : /** Set the worker pool.
618 :
619 : Replaces any existing workers with the given range. Any
620 : previous workers are released and the idle/active lists
621 : are cleared before populating with new workers.
622 :
623 : @tparam Range Forward range of pointer-like objects to worker_base.
624 :
625 : @param workers Range of workers to manage. Each element must
626 : support `std::to_address()` yielding `worker_base*`.
627 :
628 : @par Example
629 : @code
630 : std::vector<std::unique_ptr<my_worker>> workers;
631 : for(int i = 0; i < 100; ++i)
632 : workers.push_back(std::make_unique<my_worker>(ctx));
633 : srv.set_workers(std::move(workers));
634 : @endcode
635 : */
636 : template<std::ranges::forward_range Range>
637 : requires std::convertible_to<
638 : decltype(std::to_address(
639 : std::declval<std::ranges::range_value_t<Range>&>())),
640 : worker_base*>
641 : void
642 9 : set_workers(Range&& workers)
643 : {
644 : // Clear existing state
645 9 : storage_.reset();
646 9 : idle_head_ = nullptr;
647 9 : active_head_ = nullptr;
648 9 : active_tail_ = nullptr;
649 :
650 : // Take ownership and populate idle list
651 : using StorageType = std::decay_t<Range>;
652 9 : auto* p = new StorageType(std::forward<Range>(workers));
653 9 : storage_ = std::shared_ptr<void>(p, [](void* ptr) {
654 9 : delete static_cast<StorageType*>(ptr);
655 : });
656 45 : for(auto&& elem : *static_cast<StorageType*>(p))
657 36 : idle_push(std::to_address(elem));
658 9 : }
659 :
660 : /** Start accepting connections.
661 :
662 : Launches accept loops for all bound endpoints. Incoming
663 : connections are dispatched to idle workers from the pool.
664 :
665 : Calling `start()` on an already-running server has no effect.
666 :
667 : @par Preconditions
668 : - At least one endpoint bound via @ref bind.
669 : - Workers provided to the constructor.
670 : - If restarting, @ref join must have completed first.
671 :
672 : @par Effects
673 : Creates one accept coroutine per bound endpoint. Each coroutine
674 : runs on the server's executor, waiting for connections and
675 : dispatching them to idle workers.
676 :
677 : @par Restart Sequence
678 : To restart after stopping, complete the full shutdown cycle:
679 : @code
680 : srv.start();
681 : ioc.run_for( 1s );
682 : srv.stop(); // 1. Signal shutdown
683 : ioc.run(); // 2. Drain remaining completions
684 : srv.join(); // 3. Wait for accept loops
685 :
686 : // Now safe to restart
687 : srv.start();
688 : ioc.run();
689 : @endcode
690 :
691 : @par Thread Safety
692 : Not thread safe.
693 :
694 : @throws std::logic_error If a previous session has not been
695 : joined (accept loops still active).
696 : */
697 : void start();
698 :
699 : /** Stop accepting connections.
700 :
701 : Signals all listening ports to stop accepting new connections
702 : and requests cancellation of active workers via their stop tokens.
703 :
704 : This function returns immediately; it does not wait for workers
705 : to finish. Pending I/O operations complete asynchronously.
706 :
707 : Calling `stop()` on a non-running server has no effect.
708 :
709 : @par Effects
710 : - Closes all acceptors (pending accepts complete with error).
711 : - Requests stop on each active worker's stop token.
712 : - Workers observing their stop token should exit promptly.
713 :
714 : @par Postconditions
715 : No new connections will be accepted. Active workers continue
716 : until they observe their stop token or complete naturally.
717 :
718 : @par What Happens Next
719 : After calling `stop()`:
720 : 1. Let `ioc.run()` return (drains pending completions).
721 : 2. Call @ref join to wait for accept loops to finish.
722 : 3. Only then is it safe to restart or destroy the server.
723 :
724 : @par Thread Safety
725 : Not thread safe.
726 :
727 : @see join, start
728 : */
729 : void stop();
730 :
731 : /** Block until all accept loops complete.
732 :
733 : Blocks the calling thread until all accept coroutines launched
734 : by @ref start have finished executing. This synchronizes the
735 : shutdown sequence, ensuring the server is fully stopped before
736 : restarting or destroying it.
737 :
738 : @par Preconditions
739 : @ref stop has been called and `ioc.run()` has returned.
740 :
741 : @par Postconditions
742 : All accept loops have completed. The server is in the stopped
743 : state and may be restarted via @ref start.
744 :
745 : @par Example (Correct Usage)
746 : @code
747 : // main thread
748 : srv.start();
749 : ioc.run(); // Blocks until work completes
750 : srv.join(); // Safe: called after ioc.run() returns
751 : @endcode
752 :
753 : @par WARNING: Deadlock Scenarios
754 : Calling `join()` from the wrong context causes deadlock:
755 :
756 : @code
757 : // WRONG: calling join() from inside a worker coroutine
758 : void run( launcher launch ) override
759 : {
760 : launch( ex, [this]() -> capy::task<>
761 : {
762 : srv_.join(); // DEADLOCK: blocks the executor
763 : co_return;
764 : }());
765 : }
766 :
767 : // WRONG: calling join() while ioc.run() is still active
768 : std::thread t( [&]{ ioc.run(); } );
769 : srv.stop();
770 : srv.join(); // DEADLOCK: ioc.run() still running in thread t
771 : @endcode
772 :
773 : @par Thread Safety
774 : May be called from any thread, but will deadlock if called
775 : from within the io_context event loop or from a worker coroutine.
776 :
777 : @see stop, start
778 : */
779 : void join();
780 :
781 : private:
782 : capy::task<> do_stop();
783 : };
784 :
785 : #ifdef _MSC_VER
786 : #pragma warning(pop)
787 : #endif
788 :
789 : } // namespace boost::corosio
790 :
791 : #endif
|