transmission/libtransmission/session-thread.cc

299 lines
8.2 KiB
C++

// This file Copyright © Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <list>
#include <memory>
#include <mutex>
#include <thread>
#include <utility> // for std::move(), std::swap()
#include <csignal>
#ifdef _WIN32
#include <winsock2.h>
#endif
#include <event2/event.h>
#include <event2/thread.h>
#include "libtransmission/session-thread.h"
#include "libtransmission/tr-assert.h"
#include "libtransmission/utils-ev.h"
using namespace std::literals;
// ---
namespace
{
namespace tr_evthread_init_helpers
{
void* lock_alloc(unsigned /*locktype*/)
{
return new std::recursive_mutex{};
}
void lock_free(void* vlock, unsigned /*locktype*/)
{
delete static_cast<std::recursive_mutex*>(vlock);
}
int lock_lock(unsigned mode, void* vlock)
{
auto* lock = static_cast<std::recursive_mutex*>(vlock);
if ((mode & EVTHREAD_TRY) != 0U)
{
auto const success = lock->try_lock();
return success ? 0 : -1;
}
lock->lock();
return 0;
}
int lock_unlock(unsigned /*mode*/, void* vlock)
{
static_cast<std::recursive_mutex*>(vlock)->unlock();
return 0;
}
void* cond_alloc(unsigned /*condflags*/)
{
return new std::condition_variable_any();
}
void cond_free(void* vcond)
{
delete static_cast<std::condition_variable_any*>(vcond);
}
int cond_signal(void* vcond, int broadcast)
{
auto* cond = static_cast<std::condition_variable_any*>(vcond);
if (broadcast != 0)
{
cond->notify_all();
}
else
{
cond->notify_one();
}
return 0;
}
int cond_wait(void* vcond, void* vlock, struct timeval const* tv)
{
auto* cond = static_cast<std::condition_variable_any*>(vcond);
auto* lock = static_cast<std::recursive_mutex*>(vlock);
if (tv == nullptr)
{
cond->wait(*lock);
return 0;
}
auto const duration = std::chrono::seconds(tv->tv_sec) + std::chrono::microseconds(tv->tv_usec);
auto const success = cond->wait_for(*lock, duration);
return success == std::cv_status::timeout ? 1 : 0;
}
unsigned long thread_current_id()
{
thread_local auto const hashed = std::hash<std::thread::id>()(std::this_thread::get_id());
return hashed;
}
void init_evthreads_once()
{
evthread_lock_callbacks constexpr LockCbs{
EVTHREAD_LOCK_API_VERSION, EVTHREAD_LOCKTYPE_RECURSIVE, lock_alloc, lock_free, lock_lock, lock_unlock
};
evthread_set_lock_callbacks(&LockCbs);
evthread_condition_callbacks constexpr CondCbs{ EVTHREAD_CONDITION_API_VERSION,
cond_alloc,
cond_free,
cond_signal,
cond_wait };
evthread_set_condition_callbacks(&CondCbs);
evthread_set_id_callback(thread_current_id);
}
} // namespace tr_evthread_init_helpers
auto make_event_base()
{
tr_session_thread::tr_evthread_init();
return libtransmission::evhelpers::evbase_unique_ptr{ event_base_new() };
}
} // namespace
// ---
void tr_session_thread::tr_evthread_init()
{
using namespace tr_evthread_init_helpers;
static auto evthread_flag = std::once_flag{};
std::call_once(evthread_flag, init_evthreads_once);
}
class tr_session_thread_impl final : public tr_session_thread
{
public:
explicit tr_session_thread_impl()
{
auto lock = std::unique_lock(is_looping_mutex_);
thread_ = std::thread(&tr_session_thread_impl::session_thread_func, this, event_base());
thread_id_ = thread_.get_id();
// wait for the session thread's main loop to start
is_looping_cv_.wait(lock, [this]() { return is_looping_.load(); });
}
tr_session_thread_impl(tr_session_thread_impl&&) = delete;
tr_session_thread_impl(tr_session_thread_impl const&) = delete;
tr_session_thread_impl& operator=(tr_session_thread_impl&&) = delete;
tr_session_thread_impl& operator=(tr_session_thread_impl const&) = delete;
~tr_session_thread_impl() override
{
TR_ASSERT(!am_in_session_thread());
TR_ASSERT(is_looping_);
// Stop the first event loop. This is the steady-state loop that runs
// continuously, even when there are no events. See: session_thread_func()
is_shutting_down_ = true;
event_base_loopexit(event_base(), nullptr);
// Wait on the second event loop. This is the shutdown loop that exits
// as soon as there are no events. This step is to give pending tasks
// a chance to finish.
auto lock = std::unique_lock(is_looping_mutex_);
is_looping_cv_.wait_for(lock, Deadline, [this]() { return !is_looping_; });
event_base_loopexit(event_base(), nullptr);
thread_.join();
}
[[nodiscard]] struct event_base* event_base() noexcept override
{
return evbase_.get();
}
[[nodiscard]] bool am_in_session_thread() const noexcept override
{
return thread_id_ == std::this_thread::get_id();
}
void queue(std::function<void(void)>&& func) override
{
work_queue_mutex_.lock();
work_queue_.emplace_back(std::move(func));
work_queue_mutex_.unlock();
event_active(work_queue_event_.get(), 0, {});
}
void run(std::function<void(void)>&& func) override
{
if (am_in_session_thread())
{
func();
}
else
{
queue(std::move(func));
}
}
private:
using callback = std::function<void(void)>;
using work_queue_t = std::list<callback>;
void session_thread_func(struct event_base* evbase)
{
#ifndef _WIN32
/* Don't exit when writing on a broken socket */
(void)signal(SIGPIPE, SIG_IGN);
#endif
tr_evthread_init();
constexpr auto ToggleLooping = [](evutil_socket_t, short /*evtype*/, void* vself)
{
auto* const self = static_cast<tr_session_thread_impl*>(vself);
self->is_looping_mutex_.lock();
self->is_looping_ = !self->is_looping_;
self->is_looping_mutex_.unlock();
self->is_looping_cv_.notify_one();
};
event_base_once(evbase, -1, EV_TIMEOUT, ToggleLooping, this, nullptr);
// Start the first event loop. This is the steady-state loop that runs
// continuously until `this` is destroyed. See: ~tr_session_thread_impl()
TR_ASSERT(!is_shutting_down_);
event_base_loop(evbase, EVLOOP_NO_EXIT_ON_EMPTY);
// Start the second event loop. This is the shutdown loop that exits as
// soon as there are no events. It's used to give any remaining events
// a chance to finish up before we exit.
TR_ASSERT(is_shutting_down_);
event_base_loop(evbase, 0);
ToggleLooping({}, {}, this);
}
static void on_work_available_static(evutil_socket_t /*fd*/, short /*flags*/, void* vself)
{
static_cast<tr_session_thread_impl*>(vself)->on_work_available();
}
void on_work_available()
{
TR_ASSERT(am_in_session_thread());
// steal the work queue
auto work_queue_lock = std::unique_lock(work_queue_mutex_);
auto work_queue = work_queue_t{};
std::swap(work_queue, work_queue_);
work_queue_lock.unlock();
// process the work queue
for (auto const& func : work_queue)
{
func();
}
}
libtransmission::evhelpers::evbase_unique_ptr const evbase_{ make_event_base() };
libtransmission::evhelpers::event_unique_ptr const work_queue_event_{
event_new(evbase_.get(), -1, 0, on_work_available_static, this)
};
work_queue_t work_queue_;
std::mutex work_queue_mutex_;
std::thread thread_;
std::thread::id thread_id_;
std::mutex is_looping_mutex_;
std::condition_variable is_looping_cv_;
std::atomic<bool> is_looping_ = false;
std::atomic<bool> is_shutting_down_ = false;
static constexpr std::chrono::seconds Deadline = 5s;
};
std::unique_ptr<tr_session_thread> tr_session_thread::create()
{
return std::make_unique<tr_session_thread_impl>();
}