LCOV - code coverage report
Current view: top level - asio - workers.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 99.2 % 129 128
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 19 19

            Line data    Source code
       1              : #include "workers.hpp"
       2              : #include <algorithm>
       3              : #include <stdexcept>
       4              : #include <pthread.h>
       5              : #include <boost/asio/error.hpp>
       6              : #include <boost/asio/executor_work_guard.hpp>
       7              : #include <boost/asio/post.hpp>
       8              : #include <boost/asio/signal_set.hpp>
       9              : #include "../util/logger.hpp"
      10              : 
      11              : namespace thinger::asio{
      12              : 
      13              :     namespace {
      14              : 
      15              :     // The workers that started the caller thread, and its io_context: set by the thread itself,
      16              :     // before any other work, so it is read without a lock
      17              :     struct thread_mark {
      18              :         const workers* owner = nullptr;
      19              :         boost::asio::io_context* context = nullptr;
      20              :     };
      21              : 
      22        21323 :     thread_mark& this_thread_mark() {
      23              :         static thread_local thread_mark mark;
      24        21323 :         return mark;
      25              :     }
      26              : 
      27              :     // The name of the caller thread, as the system shows it (truncated to its 15 characters)
      28         6959 :     void name_this_thread(const std::string& name) {
      29         6959 :         auto truncated = name.substr(0, 15);
      30              : #if defined(__APPLE__)
      31              :         pthread_setname_np(truncated.c_str());
      32              : #elif defined(__linux__)
      33         6959 :         pthread_setname_np(pthread_self(), truncated.c_str());
      34              : #endif
      35         6959 :     }
      36              : 
      37              :     }
      38              : 
      39         1237 :     workers::workers() = default;
      40              : 
      41         1237 :     workers::~workers()
      42              :     {
      43         1237 :         if(running_) {
      44            3 :             LOG_WARNING("workers destructor called while still running - forcing stop");
      45            3 :             stop();
      46              :         }
      47              :         // A stop() called from one of its threads may still be joining the others
      48              :         {
      49         1237 :             std::unique_lock<std::mutex> lock(mutex_);
      50         2477 :             stopped_.wait(lock, [this] { return stopping_ == 0; });
      51         1237 :         }
      52              :         // Those that called stop() themselves, then the io_contexts with what they left pending
      53         8196 :         for(auto const& thread : stopped_threads_){
      54         6959 :             thread->stop();
      55              :         }
      56         1237 :         stopped_threads_.clear();
      57         1237 :     }
      58              : 
      59         4897 :     boost::system::error_code workers::start(size_t threads)
      60              :     {
      61         4897 :         std::scoped_lock<std::mutex> lock(mutex_);
      62         4897 :         if(running_) return boost::asio::error::already_started;
      63         4894 :         if(threads == 0) return make_error_code(boost::system::errc::invalid_argument);
      64              : 
      65         4891 :         LOG_DEBUG("starting {} working threads in the shared pool", threads);
      66         4891 :         auto table = std::make_unique<pool_table>();
      67         4891 :         worker_threads_.reserve(threads);
      68        11796 :         for(size_t thread_number=1; thread_number<=threads; ++thread_number){
      69         6905 :             auto worker = start_thread("worker thread " + std::to_string(thread_number));
      70         6905 :             table->contexts.push_back(&worker->get_io_context());
      71         6905 :             worker_threads_.emplace_back(std::move(worker));
      72         6905 :         }
      73              : 
      74         4891 :         next_io_context_.store(0, std::memory_order_relaxed);
      75         4891 :         table_.store(table.get());
      76         4891 :         tables_.emplace_back(std::move(table));
      77         4891 :         running_ = true;
      78         4891 :         return {};
      79         4897 :     }
      80              : 
      81         6959 :     std::unique_ptr<worker_thread> workers::start_thread(std::string name)
      82              :     {
      83         6959 :         auto worker = std::make_unique<worker_thread>(name);
      84         6959 :         auto& context = worker->get_io_context();
      85              :         // The first work of the thread: it marks itself as one of these workers
      86         6959 :         boost::asio::post(context, [this, &context, name = std::move(name)] {
      87         6959 :             this_thread_mark() = {this, &context};
      88         6959 :             name_this_thread(name);
      89         6959 :         });
      90         6959 :         worker->start();
      91         6959 :         return worker;
      92            0 :     }
      93              : 
      94           60 :     boost::asio::any_io_executor workers::isolated_executor(std::string name)
      95              :     {
      96           60 :         std::scoped_lock<std::mutex> lock(mutex_);
      97           60 :         if(!running_) return {};
      98           54 :         LOG_DEBUG("starting '{}' worker thread", name);
      99           54 :         auto worker = start_thread(std::move(name));
     100           54 :         auto executor = worker->get_io_context().get_executor();
     101           54 :         isolated_threads_.emplace_back(std::move(worker));
     102           54 :         return executor;
     103           60 :     }
     104              : 
     105         4906 :     void workers::stop()
     106              :     {
     107         4906 :         std::vector<std::unique_ptr<worker_thread>> threads;
     108              :         {
     109         4906 :             std::scoped_lock<std::mutex> lock(mutex_);
     110         4906 :             if(!running_) {
     111           15 :                 LOG_WARNING("workers already stopped");
     112           15 :                 return;
     113              :             }
     114         4891 :             running_ = false;
     115         4891 :             ++stopping_;
     116         4891 :             table_.store(nullptr);
     117         4909 :             for(auto* wait : waits_) wait->stop();
     118         4891 :             threads = std::move(isolated_threads_);
     119         4891 :             isolated_threads_.clear();
     120         4891 :             std::move(worker_threads_.begin(), worker_threads_.end(), std::back_inserter(threads));
     121         4891 :             worker_threads_.clear();
     122         4906 :         }
     123              : 
     124              :         // Out of the lock, as a handler finishing meanwhile may call back into the workers:
     125              :         // every io_context stops at once, then each thread is joined, but the caller's own
     126         4891 :         LOG_DEBUG("stopping {} worker threads", threads.size());
     127        11850 :         for(auto const& thread : threads){
     128         6959 :             thread->get_io_context().stop();
     129              :         }
     130              :         // Then the pending handlers of each are destroyed; the io_contexts are kept (see stop()
     131              :         // in workers.hpp)
     132         4891 :         auto* own_context = this_thread_mark().owner == this ? this_thread_mark().context : nullptr;
     133        11850 :         for(auto const& thread : threads){
     134         6959 :             if(&thread->get_io_context() == own_context) continue;
     135         6884 :             thread->stop();
     136         6884 :             thread->shutdown();
     137              :         }
     138              : 
     139         4891 :         std::scoped_lock<std::mutex> lock(mutex_);
     140         4891 :         std::move(threads.begin(), threads.end(), std::back_inserter(stopped_threads_));
     141         4891 :         --stopping_;
     142         4891 :         stopped_.notify_all();
     143         4891 :         LOG_DEBUG("all done!");
     144         4906 :     }
     145              : 
     146           42 :     int workers::wait(std::set<int> signals)
     147              :     {
     148           42 :         boost::asio::io_context context{1};
     149              :         // With no signal, it waits for stop()
     150           42 :         auto work = boost::asio::make_work_guard(context);
     151           42 :         boost::asio::signal_set signal_set(context);
     152           90 :         for(auto signal : signals){
     153           48 :             signal_set.add(signal);
     154              :         }
     155              : 
     156           42 :         int received = 0;
     157           42 :         signal_set.async_wait([&](const boost::system::error_code& ec, int signal_number){
     158           21 :             if(ec) return;
     159           21 :             LOG_INFO("received signal {}", signal_number);
     160           21 :             received = signal_number;
     161           21 :             context.stop();
     162              :         });
     163              : 
     164              :         {
     165           42 :             std::scoped_lock<std::mutex> lock(mutex_);
     166           42 :             if(!running_) return 0;
     167           39 :             waits_.push_back(&context);
     168           42 :         }
     169           39 :         context.run();
     170              :         {
     171           39 :             std::scoped_lock<std::mutex> lock(mutex_);
     172           39 :             std::erase(waits_, &context);
     173           39 :         }
     174           39 :         return received;
     175           42 :     }
     176              : 
     177       941664 :     boost::asio::io_context* workers::next_context()
     178              :     {
     179       941664 :         auto* table = table_.load();
     180       941664 :         if(!table) return nullptr;
     181       286618 :         auto next = next_io_context_.fetch_add(1, std::memory_order_relaxed);
     182       286618 :         return table->contexts[next % table->contexts.size()];
     183              :     }
     184              : 
     185       941199 :     boost::asio::any_io_executor workers::next_executor()
     186              :     {
     187       941199 :         auto* context = next_context();
     188       941199 :         if(!context) return {};
     189       286159 :         return context->get_executor();
     190              :     }
     191              : 
     192        14288 :     std::function<boost::asio::any_io_executor()> workers::provider()
     193              :     {
     194        43478 :         return [this] { return next_executor(); };
     195              :     }
     196              : 
     197          465 :         boost::asio::io_context& workers::get_next_io_context()
     198              :         {
     199          465 :         auto* context = next_context();
     200          465 :         if(!context) throw std::logic_error("thinger::asio::workers: not running");
     201          459 :         return *context;
     202              :         }
     203              : 
     204         5500 :         boost::asio::io_context& workers::get_thread_io_context()
     205              :         {
     206              :         // One of its threads: its io_context lives as long as the workers
     207         5500 :         const auto& mark = this_thread_mark();
     208         5500 :         if(mark.owner == this) return *mark.context;
     209              : 
     210              :         // Any other thread: the first thread of the pool
     211          961 :         auto* table = table_.load();
     212          961 :         if(!table) throw std::logic_error("thinger::asio::workers: not running");
     213          955 :         return *table->contexts.front();
     214              :         }
     215              : 
     216         3898 :     bool workers::in_worker_thread() const
     217              :     {
     218         3898 :         return this_thread_mark().owner == this && running_;
     219              :     }
     220              : 
     221              : }
        

Generated by: LCOV version 2.0-1