init
This commit is contained in:
commit
6d96c712eb
40
fibers/scheduler/exe/coroutine/impl.cpp
Normal file
40
fibers/scheduler/exe/coroutine/impl.cpp
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
#include <exe/coroutine/impl.hpp>
|
||||||
|
|
||||||
|
#include <wheels/support/assert.hpp>
|
||||||
|
#include <wheels/support/compiler.hpp>
|
||||||
|
|
||||||
|
namespace exe::coroutine {
|
||||||
|
|
||||||
|
CoroutineImpl::CoroutineImpl(Routine routine, wheels::MutableMemView stack)
|
||||||
|
: routine_(std::move(routine)) {
|
||||||
|
coroutine_context_.Setup(stack, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CoroutineImpl::Run() {
|
||||||
|
try {
|
||||||
|
routine_();
|
||||||
|
} catch (...) {
|
||||||
|
exception_ptr_ = std::current_exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
is_complete_ = true;
|
||||||
|
coroutine_context_.SwitchTo(save_previous_context_);
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CoroutineImpl::Resume() {
|
||||||
|
save_previous_context_.SwitchTo(coroutine_context_);
|
||||||
|
if (exception_ptr_) {
|
||||||
|
rethrow_exception(exception_ptr_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CoroutineImpl::Suspend() {
|
||||||
|
coroutine_context_.SwitchTo(save_previous_context_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CoroutineImpl::IsCompleted() const {
|
||||||
|
return is_complete_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace exe::coroutine
|
43
fibers/scheduler/exe/coroutine/impl.hpp
Normal file
43
fibers/scheduler/exe/coroutine/impl.hpp
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/coroutine/routine.hpp>
|
||||||
|
|
||||||
|
#include <context/context.hpp>
|
||||||
|
|
||||||
|
#include <wheels/memory/view.hpp>
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
namespace exe::coroutine {
|
||||||
|
|
||||||
|
// Stackful asymmetric coroutine impl
|
||||||
|
// - Does not manage stacks
|
||||||
|
// - Unsafe Suspend
|
||||||
|
// Base for standalone coroutines, processors, fibers
|
||||||
|
|
||||||
|
class CoroutineImpl : public ::context::ITrampoline {
|
||||||
|
public:
|
||||||
|
CoroutineImpl(Routine routine, wheels::MutableMemView stack);
|
||||||
|
|
||||||
|
// Context: Caller
|
||||||
|
void Resume();
|
||||||
|
|
||||||
|
// Context: Coroutine
|
||||||
|
void Suspend();
|
||||||
|
|
||||||
|
// Context: Caller
|
||||||
|
bool IsCompleted() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// context::ITrampoline
|
||||||
|
[[noreturn]] void Run() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Routine routine_;
|
||||||
|
context::ExecutionContext coroutine_context_;
|
||||||
|
context::ExecutionContext save_previous_context_;
|
||||||
|
std::exception_ptr exception_ptr_;
|
||||||
|
bool is_complete_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::coroutine
|
9
fibers/scheduler/exe/coroutine/routine.hpp
Normal file
9
fibers/scheduler/exe/coroutine/routine.hpp
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <wheels/support/function.hpp>
|
||||||
|
|
||||||
|
namespace exe::coroutine {
|
||||||
|
|
||||||
|
using Routine = wheels::UniqueFunction<void()>;
|
||||||
|
|
||||||
|
} // namespace exe::coroutine
|
36
fibers/scheduler/exe/coroutine/standalone.cpp
Normal file
36
fibers/scheduler/exe/coroutine/standalone.cpp
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
#include <exe/coroutine/standalone.hpp>
|
||||||
|
|
||||||
|
#include <twist/util/thread_local.hpp>
|
||||||
|
|
||||||
|
#include <wheels/support/assert.hpp>
|
||||||
|
#include <wheels/support/defer.hpp>
|
||||||
|
|
||||||
|
namespace exe::coroutine {
|
||||||
|
|
||||||
|
static twist::util::ThreadLocalPtr<Coroutine> current;
|
||||||
|
|
||||||
|
Coroutine::Coroutine(Routine routine)
|
||||||
|
: stack_(AllocateStack()), impl_(std::move(routine), stack_.View()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Coroutine::Resume() {
|
||||||
|
Coroutine* prev = current.Exchange(this);
|
||||||
|
|
||||||
|
wheels::Defer rollback([prev]() {
|
||||||
|
current = prev;
|
||||||
|
});
|
||||||
|
|
||||||
|
impl_.Resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Coroutine::Suspend() {
|
||||||
|
WHEELS_VERIFY(current, "Not a coroutine");
|
||||||
|
current->impl_.Suspend();
|
||||||
|
}
|
||||||
|
|
||||||
|
context::Stack Coroutine::AllocateStack() {
|
||||||
|
static const size_t kStackPages = 16; // 16 * 4KB = 64KB
|
||||||
|
return context::Stack::AllocatePages(kStackPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace exe::coroutine
|
30
fibers/scheduler/exe/coroutine/standalone.hpp
Normal file
30
fibers/scheduler/exe/coroutine/standalone.hpp
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#include <exe/coroutine/impl.hpp>
|
||||||
|
|
||||||
|
#include <context/stack.hpp>
|
||||||
|
|
||||||
|
namespace exe::coroutine {
|
||||||
|
|
||||||
|
// Standalone coroutine
|
||||||
|
|
||||||
|
class Coroutine {
|
||||||
|
public:
|
||||||
|
explicit Coroutine(Routine routine);
|
||||||
|
|
||||||
|
void Resume();
|
||||||
|
|
||||||
|
// Suspend current coroutine
|
||||||
|
static void Suspend();
|
||||||
|
|
||||||
|
bool IsCompleted() {
|
||||||
|
return impl_.IsCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static context::Stack AllocateStack();
|
||||||
|
|
||||||
|
private:
|
||||||
|
context::Stack stack_;
|
||||||
|
CoroutineImpl impl_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::coroutine
|
43
fibers/scheduler/exe/executors/execute.hpp
Normal file
43
fibers/scheduler/exe/executors/execute.hpp
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/executors/executor.hpp>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace exe::executors {
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
template <typename T>
|
||||||
|
struct TaskBaseFunction : TaskBase {
|
||||||
|
explicit TaskBaseFunction(T&& task) : task_(std::move(task)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Run() noexcept override {
|
||||||
|
try {
|
||||||
|
task_();
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
|
Discard();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Discard() noexcept override {
|
||||||
|
delete this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
T task_;
|
||||||
|
};
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Usage:
|
||||||
|
* Execute(thread_pool, []() {
|
||||||
|
* std::cout << "Hi" << std::endl;
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
void Execute(IExecutor& where, F&& f, Hint hint = Hint::UpToYou) {
|
||||||
|
where.Execute(new detail::TaskBaseFunction(std::move(f)), hint);
|
||||||
|
}
|
||||||
|
} // namespace exe::executors
|
19
fibers/scheduler/exe/executors/executor.hpp
Normal file
19
fibers/scheduler/exe/executors/executor.hpp
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/executors/task.hpp>
|
||||||
|
|
||||||
|
namespace exe::executors {
|
||||||
|
|
||||||
|
enum class Hint {
|
||||||
|
UpToYou = 1, // Rely on executor scheduling decision
|
||||||
|
Next = 2, // Use LIFO scheduling
|
||||||
|
Yield = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IExecutor {
|
||||||
|
virtual ~IExecutor() = default;
|
||||||
|
|
||||||
|
virtual void Execute(TaskBase* task, Hint hint) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::executors
|
19
fibers/scheduler/exe/executors/task.hpp
Normal file
19
fibers/scheduler/exe/executors/task.hpp
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <wheels/intrusive/forward_list.hpp>
|
||||||
|
#include <functional>
|
||||||
|
#include <wheels/support/function.hpp>
|
||||||
|
|
||||||
|
namespace exe::executors {
|
||||||
|
|
||||||
|
struct ITask {
|
||||||
|
virtual ~ITask() = default;
|
||||||
|
|
||||||
|
virtual void Run() noexcept = 0;
|
||||||
|
virtual void Discard() noexcept = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intrusive task
|
||||||
|
struct TaskBase : ITask, wheels::IntrusiveForwardListNode<TaskBase> {};
|
||||||
|
|
||||||
|
} // namespace exe::executors
|
10
fibers/scheduler/exe/executors/thread_pool.hpp
Normal file
10
fibers/scheduler/exe/executors/thread_pool.hpp
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/executors/tp/fast/thread_pool.hpp>
|
||||||
|
|
||||||
|
namespace exe::executors {
|
||||||
|
|
||||||
|
// Default thread pool
|
||||||
|
using ThreadPool = tp::fast::ThreadPool;
|
||||||
|
|
||||||
|
} // namespace exe::executors
|
78
fibers/scheduler/exe/executors/tp/compute/blocking_queue.hpp
Normal file
78
fibers/scheduler/exe/executors/tp/compute/blocking_queue.hpp
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <twist/stdlike/mutex.hpp>
|
||||||
|
#include <twist/stdlike/atomic.hpp>
|
||||||
|
#include <twist/stdlike/condition_variable.hpp>
|
||||||
|
#include <exe/executors/task.hpp>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
namespace exe::executors {
|
||||||
|
class UnboundedBlockingQueue {
|
||||||
|
using T = TaskBase*;
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool Put(T value) {
|
||||||
|
std::lock_guard guard(mutex_);
|
||||||
|
|
||||||
|
if (is_closed_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
not_empty_or_closed_.notify_one();
|
||||||
|
deque_.emplace_back(std::move(value));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<T> Take() {
|
||||||
|
std::unique_lock lock(mutex_);
|
||||||
|
|
||||||
|
while (deque_.empty()) {
|
||||||
|
if (is_closed_) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
not_empty_or_closed_.wait(lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Close() {
|
||||||
|
CloseImpl(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cancel() {
|
||||||
|
CloseImpl(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::optional<T> GetValue() {
|
||||||
|
if (deque_.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
T result = std::move(deque_.front());
|
||||||
|
deque_.pop_front();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CloseImpl(bool clear) {
|
||||||
|
std::lock_guard guard(mutex_);
|
||||||
|
is_closed_ = true;
|
||||||
|
for (auto& i : deque_) {
|
||||||
|
i->Discard();
|
||||||
|
}
|
||||||
|
if (clear) [[likely]] { // NOLINT
|
||||||
|
deque_.clear();
|
||||||
|
}
|
||||||
|
not_empty_or_closed_.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_closed_ = false;
|
||||||
|
twist::stdlike::condition_variable not_empty_or_closed_;
|
||||||
|
twist::stdlike::mutex mutex_;
|
||||||
|
std::deque<T> deque_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::executors
|
71
fibers/scheduler/exe/executors/tp/compute/thread_pool.cpp
Normal file
71
fibers/scheduler/exe/executors/tp/compute/thread_pool.cpp
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
#include <exe/executors/tp/compute/thread_pool.hpp>
|
||||||
|
#include <utility>
|
||||||
|
#include <cassert>
|
||||||
|
#include <wheels/logging/logging.hpp>
|
||||||
|
|
||||||
|
#include <twist/util/thread_local.hpp>
|
||||||
|
|
||||||
|
namespace exe::executors::tp::compute {
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
static twist::util::ThreadLocalPtr<ThreadPool> pool;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
ThreadPool::ThreadPool(size_t workers) : wait_end_tasks_(0) {
|
||||||
|
for (size_t i = 0; i < workers; ++i) {
|
||||||
|
threads_.emplace_back(std::bind(&ThreadPool::Worker, this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExecuteTask(TaskBase* task) {
|
||||||
|
try {
|
||||||
|
task->Run();
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadPool::Worker() {
|
||||||
|
pool = this;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
auto task = queue_.Take();
|
||||||
|
if (!task) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecuteTask(*task);
|
||||||
|
wait_end_tasks_.Done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadPool::~ThreadPool() {
|
||||||
|
assert(threads_.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadPool::Execute(TaskBase* task, Hint) {
|
||||||
|
wait_end_tasks_.Add();
|
||||||
|
if (!queue_.Put(task)) {
|
||||||
|
task->Discard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadPool::WaitIdle() {
|
||||||
|
wait_end_tasks_.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadPool::Stop() {
|
||||||
|
queue_.Cancel();
|
||||||
|
for (auto& i : threads_) {
|
||||||
|
i.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
threads_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadPool* ThreadPool::Current() {
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace exe::executors::tp::compute
|
45
fibers/scheduler/exe/executors/tp/compute/thread_pool.hpp
Normal file
45
fibers/scheduler/exe/executors/tp/compute/thread_pool.hpp
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/executors/executor.hpp>
|
||||||
|
#include <twist/stdlike/thread.hpp>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <exe/support/wait_group.hpp>
|
||||||
|
#include <exe/executors/tp/compute/blocking_queue.hpp>
|
||||||
|
|
||||||
|
namespace exe::executors::tp::compute {
|
||||||
|
|
||||||
|
// Fixed-size pool of worker threads
|
||||||
|
|
||||||
|
class ThreadPool : public IExecutor {
|
||||||
|
public:
|
||||||
|
explicit ThreadPool(size_t workers);
|
||||||
|
~ThreadPool();
|
||||||
|
|
||||||
|
// Non-copyable
|
||||||
|
ThreadPool(const ThreadPool&) = delete;
|
||||||
|
ThreadPool& operator=(const ThreadPool&) = delete;
|
||||||
|
|
||||||
|
// IExecutor
|
||||||
|
void Execute(TaskBase* task, Hint) override;
|
||||||
|
|
||||||
|
// Waits until outstanding work count has reached zero
|
||||||
|
void WaitIdle();
|
||||||
|
|
||||||
|
// Stops the worker threads as soon as possible
|
||||||
|
// Pending tasks will be discarded
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
// Locates current thread pool from worker thread
|
||||||
|
static ThreadPool* Current();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Worker();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<twist::stdlike::thread> threads_;
|
||||||
|
UnboundedBlockingQueue queue_;
|
||||||
|
support::WaitGroup wait_end_tasks_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::executors::tp::compute
|
30
fibers/scheduler/exe/executors/tp/fast/coordinator.cpp
Normal file
30
fibers/scheduler/exe/executors/tp/fast/coordinator.cpp
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#include <exe/executors/tp/fast/coordinator.hpp>
|
||||||
|
#include <exe/executors/tp/fast/thread_pool.hpp>
|
||||||
|
|
||||||
|
namespace exe::executors::tp::fast {
|
||||||
|
Coordinator::Coordinator(ThreadPool& host, size_t workers)
|
||||||
|
: host_(host), workers_(workers), active_workers_(workers) {
|
||||||
|
for (size_t i = 0; i < workers; ++i) {
|
||||||
|
states_.emplace_back(WorkerState::kWorking);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Coordinator::StopSpinning() {
|
||||||
|
if (spinning_workers_.fetch_sub(1) == 1) {
|
||||||
|
if (available_tasks_.load() > active_workers_.load()) {
|
||||||
|
NotifyOneWorker(host_.workers_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Coordinator::StartWorking(size_t index) {
|
||||||
|
if (states_[index].exchange(WorkerState::kWorking) == WorkerState::kWorking) {
|
||||||
|
NotifyOneWorker(host_.workers_);
|
||||||
|
}
|
||||||
|
active_workers_.fetch_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Coordinator::WakeUp() {
|
||||||
|
active_workers_.fetch_add(1);
|
||||||
|
}
|
||||||
|
} // namespace exe::executors::tp::fast
|
100
fibers/scheduler/exe/executors/tp/fast/coordinator.hpp
Normal file
100
fibers/scheduler/exe/executors/tp/fast/coordinator.hpp
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <twist/stdlike/atomic.hpp>
|
||||||
|
#include <exe/support/wait_group.hpp>
|
||||||
|
#include <wheels/logging/logging.hpp>
|
||||||
|
#include <twist/stdlike/mutex.hpp>
|
||||||
|
#include <twist/stdlike/condition_variable.hpp>
|
||||||
|
#include <wheels/intrusive/list.hpp>
|
||||||
|
#include <exe/executors/tp/fast/worker.hpp>
|
||||||
|
#include <exe/support/spinlock.hpp>
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
namespace exe::executors::tp::fast {
|
||||||
|
|
||||||
|
// Coordinates workers (stealing, parking)
|
||||||
|
|
||||||
|
enum class WorkerState : uint8_t { kWorking, kSleeping };
|
||||||
|
|
||||||
|
class Coordinator {
|
||||||
|
public:
|
||||||
|
Coordinator(ThreadPool& host, size_t workers);
|
||||||
|
|
||||||
|
void StartSpinning() {
|
||||||
|
spinning_workers_.fetch_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopSpinning();
|
||||||
|
|
||||||
|
bool StartSleeping(size_t index) {
|
||||||
|
if (is_stopped.load()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active_workers_.fetch_sub(1) <= available_tasks_.load()) {
|
||||||
|
active_workers_.fetch_add(1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
states_[index].store(WorkerState::kSleeping);
|
||||||
|
|
||||||
|
StopSpinning();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StartWorking(size_t index);
|
||||||
|
|
||||||
|
void WakeUp();
|
||||||
|
|
||||||
|
void NotifyOneWorker(std::deque<Worker>& workers) {
|
||||||
|
if (active_workers_.load() == workers_ || spinning_workers_.load() != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < workers_; ++i) {
|
||||||
|
if (states_[i].exchange(WorkerState::kWorking) ==
|
||||||
|
WorkerState::kSleeping) {
|
||||||
|
workers[i].Notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotifyAllWorkers(std::deque<Worker>& workers) {
|
||||||
|
for (size_t i = 0; i < workers_; ++i) {
|
||||||
|
workers[i].Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaitIdle() {
|
||||||
|
wait_finished_tasks_.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PushedTask() {
|
||||||
|
wait_finished_tasks_.Add();
|
||||||
|
available_tasks_.fetch_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunningTask() {
|
||||||
|
available_tasks_.fetch_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FinishTask() {
|
||||||
|
wait_finished_tasks_.Done();
|
||||||
|
}
|
||||||
|
|
||||||
|
twist::stdlike::atomic<bool> is_stopped{false};
|
||||||
|
|
||||||
|
private:
|
||||||
|
ThreadPool& host_;
|
||||||
|
size_t workers_ = 0;
|
||||||
|
twist::stdlike::atomic<size_t> active_workers_{0};
|
||||||
|
twist::stdlike::atomic<size_t> spinning_workers_{0};
|
||||||
|
twist::stdlike::atomic<size_t> available_tasks_{0};
|
||||||
|
std::deque<twist::stdlike::atomic<WorkerState>> states_;
|
||||||
|
|
||||||
|
support::WaitGroup wait_finished_tasks_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::executors::tp::fast
|
23
fibers/scheduler/exe/executors/tp/fast/metrics.hpp
Normal file
23
fibers/scheduler/exe/executors/tp/fast/metrics.hpp
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace exe::executors::tp::fast {
|
||||||
|
|
||||||
|
struct WorkerMetrics {
|
||||||
|
size_t count_solved_tasks = 0;
|
||||||
|
size_t count_pick_lifo_slot = 0;
|
||||||
|
size_t count_task_from_local_queue = 0;
|
||||||
|
size_t count_grab_from_global_queue = 0;
|
||||||
|
size_t count_park = 0;
|
||||||
|
size_t count_spinning = 0;
|
||||||
|
size_t count_offload = 0;
|
||||||
|
size_t count_sleep_rejects = 0;
|
||||||
|
size_t count_stiling = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PoolMetrics : WorkerMetrics {
|
||||||
|
// Your metrics goes here
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::executors::tp::fast
|
|
@ -0,0 +1,71 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/executors/task.hpp>
|
||||||
|
|
||||||
|
#include <wheels/intrusive/forward_list.hpp>
|
||||||
|
#include <wheels/logging/logging.hpp>
|
||||||
|
|
||||||
|
#include <twist/stdlike/mutex.hpp>
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
#include <exe/support/spinlock.hpp>
|
||||||
|
|
||||||
|
namespace exe::executors::tp::fast {
|
||||||
|
|
||||||
|
// Unbounded queue shared between workers
|
||||||
|
|
||||||
|
class GlobalQueue {
|
||||||
|
public:
|
||||||
|
void PushOne(TaskBase* item) {
|
||||||
|
std::lock_guard lock(guard_queue_);
|
||||||
|
|
||||||
|
queue_.PushBack(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PushList(wheels::IntrusiveForwardList<TaskBase>& list) {
|
||||||
|
std::lock_guard lock(guard_queue_);
|
||||||
|
queue_.Append(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Offload(std::span<TaskBase*> buffer) {
|
||||||
|
wheels::IntrusiveForwardList<TaskBase> list;
|
||||||
|
for (auto& i : buffer) {
|
||||||
|
list.PushBack(i);
|
||||||
|
}
|
||||||
|
PushList(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns nullptr if queue is empty
|
||||||
|
TaskBase* TryPopOne() {
|
||||||
|
std::lock_guard lock(guard_queue_);
|
||||||
|
|
||||||
|
return queue_.PopFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns number of items in `out_buffer`
|
||||||
|
size_t Grab(std::span<TaskBase*> out_buffer, size_t workers) {
|
||||||
|
std::lock_guard lock(guard_queue_);
|
||||||
|
|
||||||
|
size_t grab_size = (queue_.Size() + workers - 1) / workers;
|
||||||
|
|
||||||
|
size_t current_grab = 0;
|
||||||
|
for (auto& i : out_buffer) {
|
||||||
|
i = queue_.PopFront();
|
||||||
|
if (i == nullptr) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current_grab++;
|
||||||
|
if (current_grab == grab_size) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return current_grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
support::SpinLock guard_queue_;
|
||||||
|
wheels::IntrusiveForwardList<TaskBase> queue_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::executors::tp::fast
|
|
@ -0,0 +1,130 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <twist/stdlike/atomic.hpp>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <span>
|
||||||
|
#include <wheels/logging/logging.hpp>
|
||||||
|
|
||||||
|
#include <exe/executors/task.hpp>
|
||||||
|
|
||||||
|
namespace exe::executors::tp::fast {
|
||||||
|
|
||||||
|
// Single producer / multiple consumers bounded queue
|
||||||
|
// for local tasks
|
||||||
|
|
||||||
|
template <size_t Capacity>
|
||||||
|
class WorkStealingQueue {
|
||||||
|
// using T = TaskBase;
|
||||||
|
using T = TaskBase;
|
||||||
|
using Slot = T*;
|
||||||
|
|
||||||
|
public:
|
||||||
|
WorkStealingQueue() {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TryPush(T* item) {
|
||||||
|
size_t old_head = head_.load(std::memory_order::relaxed);
|
||||||
|
size_t old_tail = tail_.load(std::memory_order::relaxed);
|
||||||
|
|
||||||
|
if (IsFull(old_head, old_tail)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer_[old_tail % Capacity].store(item, std::memory_order::relaxed);
|
||||||
|
tail_.store(old_tail + 1, std::memory_order::release);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PushManyTasks(std::span<T*> tasks) {
|
||||||
|
size_t old_head = head_.load(std::memory_order::acquire);
|
||||||
|
size_t old_tail = tail_.load(std::memory_order::relaxed);
|
||||||
|
|
||||||
|
if (Capacity - Size(old_head, old_tail) < tasks.size()) {
|
||||||
|
WHEELS_PANIC("In work stealing queue is no free place");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < tasks.size(); ++i) {
|
||||||
|
buffer_[(old_tail + i) % Capacity].store(tasks[i],
|
||||||
|
std::memory_order::relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
tail_.store(old_tail + tasks.size(), std::memory_order::release);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns nullptr if queue is empty
|
||||||
|
T* TryPop() {
|
||||||
|
T* result = nullptr;
|
||||||
|
if (GrabImpl({&result, &result + 1}, 1, std::memory_order::relaxed) == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns number of tasks
|
||||||
|
size_t Grab(std::span<T*> out_buffer) {
|
||||||
|
return GrabImpl(out_buffer, 1, std::memory_order::acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t GrabPart(std::span<T*> out_buffer, size_t part) {
|
||||||
|
return GrabImpl(out_buffer, part, std::memory_order::acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DiscardAll() {
|
||||||
|
std::array<T*, Capacity> tasks;
|
||||||
|
size_t count = Grab(tasks);
|
||||||
|
for (auto task : std::span<T*>(tasks.begin(), tasks.begin() + count)) {
|
||||||
|
task->Discard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
size_t GrabImpl(std::span<T*> out_buffer, size_t part,
|
||||||
|
std::memory_order memory_order_read_tail) {
|
||||||
|
size_t size_out_buffer = out_buffer.size();
|
||||||
|
if (size_out_buffer == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t old_head = head_.load(std::memory_order::acquire);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
size_t old_tail = tail_.load(memory_order_read_tail);
|
||||||
|
size_t grab_size =
|
||||||
|
std::min(Size(old_head, old_tail) / part, size_out_buffer);
|
||||||
|
|
||||||
|
if (grab_size == 0) {
|
||||||
|
return grab_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < grab_size; ++i) {
|
||||||
|
out_buffer[i] =
|
||||||
|
buffer_[(old_head + i) % Capacity].load(std::memory_order::relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (head_.compare_exchange_strong(old_head, old_head + grab_size,
|
||||||
|
std::memory_order::release,
|
||||||
|
std::memory_order::acquire)) {
|
||||||
|
return grab_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsEmpty(size_t head, size_t tail) {
|
||||||
|
return Size(head, tail) == 0;
|
||||||
|
}
|
||||||
|
static bool IsFull(size_t head, size_t tail) {
|
||||||
|
return Size(head, tail) >= Capacity;
|
||||||
|
}
|
||||||
|
static size_t Size(size_t head, size_t tail) {
|
||||||
|
return head > tail ? 0 : tail - head;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::array<twist::stdlike::atomic<Slot>, Capacity> buffer_;
|
||||||
|
twist::stdlike::atomic<size_t> tail_{0};
|
||||||
|
twist::stdlike::atomic<size_t> head_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::executors::tp::fast
|
76
fibers/scheduler/exe/executors/tp/fast/thread_pool.cpp
Normal file
76
fibers/scheduler/exe/executors/tp/fast/thread_pool.cpp
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
#include <exe/executors/tp/fast/thread_pool.hpp>
|
||||||
|
|
||||||
|
#include <twist/util/thread_local.hpp>
|
||||||
|
|
||||||
|
namespace exe::executors::tp::fast {
|
||||||
|
|
||||||
|
ThreadPool::ThreadPool(size_t threads)
|
||||||
|
: count_threads_(threads), coordinator_(*this, threads) {
|
||||||
|
for (size_t i = 0; i < threads; ++i) {
|
||||||
|
workers_.emplace_back(*this, i);
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < threads; ++i) {
|
||||||
|
workers_[i].Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadPool::~ThreadPool() {
|
||||||
|
// if (!workers_.empty()) {
|
||||||
|
// std::abort();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadPool::Execute(TaskBase* task, Hint hint) {
|
||||||
|
coordinator_.PushedTask();
|
||||||
|
Push(task, hint);
|
||||||
|
coordinator_.NotifyOneWorker(workers_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadPool::Push(TaskBase* task, Hint hint) {
|
||||||
|
if (hint == Hint::Yield) {
|
||||||
|
global_tasks_.PushOne(task);
|
||||||
|
} else if (Worker::Current() != nullptr &&
|
||||||
|
std::addressof(Worker::Current()->Host()) == this) {
|
||||||
|
if (hint == Hint::Next) {
|
||||||
|
Worker::Current()->PushToLifoSlot(task);
|
||||||
|
} else {
|
||||||
|
Worker::Current()->PushToLocalQueue(task);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
global_tasks_.PushOne(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadPool::WaitIdle() {
|
||||||
|
coordinator_.WaitIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadPool::Stop() {
|
||||||
|
coordinator_.is_stopped.store(true); // TODO
|
||||||
|
coordinator_.NotifyAllWorkers(workers_);
|
||||||
|
for (auto& worker : workers_) {
|
||||||
|
worker.Join();
|
||||||
|
}
|
||||||
|
while (auto next = global_tasks_.TryPopOne()) {
|
||||||
|
next->Discard();
|
||||||
|
}
|
||||||
|
// workers_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<WorkerMetrics> ThreadPool::Metrics() const {
|
||||||
|
std::vector<WorkerMetrics> result;
|
||||||
|
for (auto& worker : workers_) {
|
||||||
|
result.emplace_back(worker.Metrics());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadPool* ThreadPool::Current() {
|
||||||
|
auto current_worker = Worker::Current();
|
||||||
|
if (current_worker == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return std::addressof(current_worker->Host());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace exe::executors::tp::fast
|
53
fibers/scheduler/exe/executors/tp/fast/thread_pool.hpp
Normal file
53
fibers/scheduler/exe/executors/tp/fast/thread_pool.hpp
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/executors/executor.hpp>
|
||||||
|
|
||||||
|
#include <exe/executors/tp/fast/queues/global_queue.hpp>
|
||||||
|
#include <exe/executors/tp/fast/worker.hpp>
|
||||||
|
#include <exe/executors/tp/fast/coordinator.hpp>
|
||||||
|
#include <exe/executors/tp/fast/metrics.hpp>
|
||||||
|
|
||||||
|
// random_device
|
||||||
|
#include <twist/stdlike/random.hpp>
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
namespace exe::executors::tp::fast {
|
||||||
|
|
||||||
|
// Scalable work-stealing scheduler for short-lived tasks
|
||||||
|
|
||||||
|
class ThreadPool : public IExecutor {
|
||||||
|
friend class Worker;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ThreadPool(size_t threads);
|
||||||
|
~ThreadPool();
|
||||||
|
|
||||||
|
// Non-copyable
|
||||||
|
ThreadPool(const ThreadPool&) = delete;
|
||||||
|
ThreadPool& operator=(const ThreadPool&) = delete;
|
||||||
|
|
||||||
|
// IExecutor
|
||||||
|
void Execute(TaskBase* task, Hint hint = Hint::UpToYou) override;
|
||||||
|
|
||||||
|
void WaitIdle();
|
||||||
|
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
// After Stop
|
||||||
|
std::vector<WorkerMetrics> Metrics() const;
|
||||||
|
|
||||||
|
static ThreadPool* Current();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Push(TaskBase* task, Hint hint);
|
||||||
|
|
||||||
|
private:
|
||||||
|
size_t count_threads_;
|
||||||
|
std::deque<Worker> workers_;
|
||||||
|
friend class Coordinator;
|
||||||
|
Coordinator coordinator_;
|
||||||
|
GlobalQueue global_tasks_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::executors::tp::fast
|
229
fibers/scheduler/exe/executors/tp/fast/worker.cpp
Normal file
229
fibers/scheduler/exe/executors/tp/fast/worker.cpp
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
#include <exe/executors/tp/fast/worker.hpp>
|
||||||
|
#include <exe/executors/tp/fast/thread_pool.hpp>
|
||||||
|
|
||||||
|
#include <twist/strand/thread_local.hpp>
|
||||||
|
|
||||||
|
namespace exe::executors::tp::fast {
|
||||||
|
|
||||||
|
TWIST_DECLARE_TL_PTR(Worker, worker);
|
||||||
|
|
||||||
|
Worker::Worker(ThreadPool& host, size_t index) : host_(host), index_(index) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Worker::Start() {
|
||||||
|
thread_.emplace([this]() {
|
||||||
|
worker = this;
|
||||||
|
Work();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Worker::Join() {
|
||||||
|
thread_->join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Worker::PushToLocalQueue(TaskBase* task) {
|
||||||
|
if (!local_tasks_.TryPush(task)) {
|
||||||
|
OffloadTasksToGlobalQueue(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Worker::PushToLifoSlot(TaskBase* task) {
|
||||||
|
if (lifo_slot_ != nullptr) {
|
||||||
|
PushToLocalQueue(lifo_slot_);
|
||||||
|
}
|
||||||
|
lifo_slot_ = task;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Worker::StealTasks(std::span<TaskBase*> out_buffer) {
|
||||||
|
return local_tasks_.GrabPart(out_buffer, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskBase* Worker::PickNextTask() {
|
||||||
|
// [Periodically] Global queue
|
||||||
|
// 0) LIFO slot
|
||||||
|
// 1) Local queue
|
||||||
|
// 2) Global queue
|
||||||
|
// 3) Work stealing
|
||||||
|
// 4) Park worker
|
||||||
|
|
||||||
|
TaskBase* result = TryPickNextTask();
|
||||||
|
if (result != nullptr) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
host_.coordinator_.StartSpinning();
|
||||||
|
while (true) {
|
||||||
|
++metrics_.count_spinning;
|
||||||
|
uint32_t old = wakeups_.load();
|
||||||
|
|
||||||
|
result = TryStealTasks(8);
|
||||||
|
if (result != nullptr) {
|
||||||
|
++metrics_.count_stiling;
|
||||||
|
host_.coordinator_.StopSpinning();
|
||||||
|
|
||||||
|
if (host_.coordinator_.is_stopped.load()) {
|
||||||
|
result->Discard();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((result = GrabTasksFromGlobalQueue()) != nullptr) {
|
||||||
|
host_.coordinator_.StopSpinning();
|
||||||
|
|
||||||
|
if (host_.coordinator_.is_stopped.load()) {
|
||||||
|
result->Discard();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host_.coordinator_.StartSleeping(index_)) {
|
||||||
|
result = TryPickTaskBeforePark();
|
||||||
|
|
||||||
|
if (result != nullptr) {
|
||||||
|
host_.coordinator_.StartWorking(index_);
|
||||||
|
|
||||||
|
if (host_.coordinator_.is_stopped.load()) {
|
||||||
|
result->Discard();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
++metrics_.count_park;
|
||||||
|
Park(old);
|
||||||
|
host_.coordinator_.WakeUp();
|
||||||
|
host_.coordinator_.StartSpinning();
|
||||||
|
} else {
|
||||||
|
++metrics_.count_sleep_rejects;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host_.coordinator_.is_stopped.load()) {
|
||||||
|
host_.coordinator_.StopSpinning();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Worker::Work() {
|
||||||
|
while (TaskBase* next = PickNextTask()) {
|
||||||
|
host_.coordinator_.RunningTask();
|
||||||
|
next->Run();
|
||||||
|
++metrics_.count_solved_tasks;
|
||||||
|
host_.coordinator_.FinishTask();
|
||||||
|
}
|
||||||
|
local_tasks_.DiscardAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Worker::Park(uint32_t old) {
|
||||||
|
while (wakeups_.load() == old) {
|
||||||
|
wakeups_.wait(old);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Worker* Worker::Current() {
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Worker::OffloadTasksToGlobalQueue(TaskBase* task) {
|
||||||
|
++metrics_.count_offload;
|
||||||
|
std::array<TaskBase*, kLocalQueueCapacity / 2 + 1> buffer;
|
||||||
|
size_t count = local_tasks_.Grab({buffer.begin(), buffer.end() - 1});
|
||||||
|
buffer[count] = task;
|
||||||
|
host_.global_tasks_.Offload({buffer.begin(), buffer.begin() + count + 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskBase* Worker::GrabTasksFromGlobalQueue() {
|
||||||
|
std::array<TaskBase*, kLocalQueueCapacity / 2> buffer;
|
||||||
|
|
||||||
|
size_t count = host_.global_tasks_.Grab(buffer, host_.count_threads_);
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
++metrics_.count_grab_from_global_queue;
|
||||||
|
local_tasks_.PushManyTasks({buffer.begin() + 1, buffer.begin() + count});
|
||||||
|
return buffer[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskBase* Worker::TryStealTasksFrom(size_t index_worker) {
|
||||||
|
std::array<TaskBase*, kLocalQueueCapacity / 2> buffer;
|
||||||
|
|
||||||
|
size_t count =
|
||||||
|
host_.workers_[index_worker].StealTasks({buffer.begin(), buffer.end()});
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
local_tasks_.PushManyTasks({buffer.begin() + 1, buffer.begin() + count});
|
||||||
|
|
||||||
|
return buffer[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskBase* Worker::TryStealTasks(size_t series) {
|
||||||
|
for (size_t i = 0; i < series; ++i) {
|
||||||
|
TaskBase* result = TryStealTasksFrom(twister_() % host_.count_threads_);
|
||||||
|
if (result != nullptr) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskBase* Worker::TryPickTaskBeforePark() {
|
||||||
|
TaskBase* result = GrabTasksFromGlobalQueue();
|
||||||
|
if (result != nullptr) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < host_.count_threads_; ++i) {
|
||||||
|
result = TryStealTasksFrom(i);
|
||||||
|
if (result != nullptr) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskBase* Worker::TryPickNextTask() {
|
||||||
|
if (host_.coordinator_.is_stopped.load()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskBase* result;
|
||||||
|
|
||||||
|
if (count_lifo_slot_ == 52) {
|
||||||
|
count_lifo_slot_ = 0;
|
||||||
|
if (lifo_slot_ != nullptr) {
|
||||||
|
PushToLocalQueue(std::exchange(lifo_slot_, nullptr));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
++count_lifo_slot_;
|
||||||
|
result = std::exchange(lifo_slot_, nullptr);
|
||||||
|
if (result != nullptr) {
|
||||||
|
++metrics_.count_pick_lifo_slot;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = local_tasks_.TryPop();
|
||||||
|
if (result != nullptr) {
|
||||||
|
++metrics_.count_task_from_local_queue;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((result = GrabTasksFromGlobalQueue()) != nullptr) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace exe::executors::tp::fast
|
99
fibers/scheduler/exe/executors/tp/fast/worker.hpp
Normal file
99
fibers/scheduler/exe/executors/tp/fast/worker.hpp
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/executors/task.hpp>
|
||||||
|
|
||||||
|
#include <exe/executors/tp/fast/metrics.hpp>
|
||||||
|
#include <exe/executors/tp/fast/queues/work_stealing_queue.hpp>
|
||||||
|
|
||||||
|
#include <twist/stdlike/atomic.hpp>
|
||||||
|
#include <twist/stdlike/thread.hpp>
|
||||||
|
#include <twist/stdlike/random.hpp>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <optional>
|
||||||
|
#include <random>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
namespace exe::executors::tp::fast {
|
||||||
|
|
||||||
|
class Worker;
|
||||||
|
class ThreadPool;
|
||||||
|
|
||||||
|
class Worker {
|
||||||
|
private:
|
||||||
|
static const size_t kLocalQueueCapacity = 256;
|
||||||
|
|
||||||
|
public:
|
||||||
|
Worker(ThreadPool& host, size_t index);
|
||||||
|
|
||||||
|
void Start();
|
||||||
|
void Join();
|
||||||
|
|
||||||
|
// Submit task
|
||||||
|
void PushToLocalQueue(TaskBase* task);
|
||||||
|
void PushToLifoSlot(TaskBase* task);
|
||||||
|
|
||||||
|
// Steal from this worker
|
||||||
|
size_t StealTasks(std::span<TaskBase*> out_buffer);
|
||||||
|
|
||||||
|
// Wake parked worker
|
||||||
|
void Wake();
|
||||||
|
|
||||||
|
static Worker* Current();
|
||||||
|
|
||||||
|
WorkerMetrics Metrics() const {
|
||||||
|
return metrics_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadPool& Host() const {
|
||||||
|
return host_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Notify() {
|
||||||
|
wakeups_.fetch_add(1);
|
||||||
|
wakeups_.notify_one();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void OffloadTasksToGlobalQueue(TaskBase* task);
|
||||||
|
|
||||||
|
TaskBase* TryStealTasksFrom(size_t index_worker);
|
||||||
|
TaskBase* TryStealTasks(size_t series);
|
||||||
|
TaskBase* GrabTasksFromGlobalQueue();
|
||||||
|
TaskBase* TryPickNextTask();
|
||||||
|
TaskBase* TryPickTaskBeforePark();
|
||||||
|
|
||||||
|
// Blocking
|
||||||
|
TaskBase* PickNextTask();
|
||||||
|
|
||||||
|
// Run Loop
|
||||||
|
void Work();
|
||||||
|
|
||||||
|
void Park(uint32_t);
|
||||||
|
|
||||||
|
private:
|
||||||
|
ThreadPool& host_;
|
||||||
|
const size_t index_;
|
||||||
|
|
||||||
|
// Worker thread
|
||||||
|
std::optional<twist::stdlike::thread> thread_;
|
||||||
|
|
||||||
|
// Local queue
|
||||||
|
WorkStealingQueue<kLocalQueueCapacity> local_tasks_;
|
||||||
|
|
||||||
|
// For work stealing
|
||||||
|
std::mt19937_64 twister_{twist::stdlike::random_device()()};
|
||||||
|
|
||||||
|
// LIFO slot
|
||||||
|
TaskBase* lifo_slot_ = nullptr;
|
||||||
|
|
||||||
|
// Parking lot
|
||||||
|
twist::stdlike::atomic<uint32_t> wakeups_{0};
|
||||||
|
|
||||||
|
size_t count_lifo_slot_ = 0;
|
||||||
|
|
||||||
|
WorkerMetrics metrics_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::executors::tp::fast
|
31
fibers/scheduler/exe/fibers/core/api.hpp
Normal file
31
fibers/scheduler/exe/fibers/core/api.hpp
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/coroutine/routine.hpp>
|
||||||
|
#include <exe/executors/executor.hpp>
|
||||||
|
#include <exe/fibers/core/awaiter.hpp>
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
|
||||||
|
using Routine = coroutine::Routine;
|
||||||
|
|
||||||
|
using Scheduler = executors::IExecutor;
|
||||||
|
|
||||||
|
// Considered harmful
|
||||||
|
|
||||||
|
// Starts a new fiber
|
||||||
|
void Go(Scheduler& scheduler, Routine routine);
|
||||||
|
|
||||||
|
// Starts a new fiber in the current scheduler
|
||||||
|
void Go(Routine routine);
|
||||||
|
|
||||||
|
namespace self {
|
||||||
|
|
||||||
|
void Yield(executors::Hint hint = executors::Hint::Yield);
|
||||||
|
|
||||||
|
// For synchronization primitives
|
||||||
|
// Do not use directly
|
||||||
|
void Suspend(exe::fibers::IContinuationStealingAwaiter*);
|
||||||
|
|
||||||
|
} // namespace self
|
||||||
|
|
||||||
|
} // namespace exe::fibers
|
35
fibers/scheduler/exe/fibers/core/awaiter.hpp
Normal file
35
fibers/scheduler/exe/fibers/core/awaiter.hpp
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/fibers/core/handle.hpp>
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
|
||||||
|
struct IContinuationStealingAwaiter {
|
||||||
|
virtual FiberHandle TransferTo(FiberHandle fiber_handle) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ContinuationStealingAwaiter : public IContinuationStealingAwaiter {
|
||||||
|
public:
|
||||||
|
explicit ContinuationStealingAwaiter(FiberHandle fiber_handle)
|
||||||
|
: fiber_handle_(fiber_handle) {
|
||||||
|
}
|
||||||
|
|
||||||
|
FiberHandle TransferTo(FiberHandle fiber_handle) override final {
|
||||||
|
auto safe = fiber_handle_;
|
||||||
|
fiber_handle.Schedule();
|
||||||
|
return safe;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
FiberHandle fiber_handle_;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IAwaiter : IContinuationStealingAwaiter {
|
||||||
|
virtual FiberHandle TransferTo(FiberHandle fiber_handle) override final {
|
||||||
|
OnDispatch(fiber_handle);
|
||||||
|
return FiberHandle::Invalid();
|
||||||
|
}
|
||||||
|
virtual void OnDispatch(FiberHandle fiber_handle) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::fibers
|
118
fibers/scheduler/exe/fibers/core/fiber.cpp
Normal file
118
fibers/scheduler/exe/fibers/core/fiber.cpp
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
#include <exe/fibers/core/fiber.hpp>
|
||||||
|
#include <exe/fibers/core/stacks.hpp>
|
||||||
|
#include <exe/executors/execute.hpp>
|
||||||
|
|
||||||
|
#include <twist/util/thread_local.hpp>
|
||||||
|
#include <wheels/logging/logging.hpp>
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
namespace detail {
|
||||||
|
void FiberTask::Run() noexcept {
|
||||||
|
// static_cast<exe::fibers::Fiber*>(this)->Resume();
|
||||||
|
auto fiber_handle = static_cast<exe::fibers::Fiber*>(this)->Resume();
|
||||||
|
if (fiber_handle.IsValid()) {
|
||||||
|
fiber_handle.Resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FiberTask::Discard() noexcept {
|
||||||
|
delete static_cast<exe::fibers::Fiber*>(this);
|
||||||
|
}
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
struct YieldAwaiter : IAwaiter {
|
||||||
|
explicit YieldAwaiter(executors::Hint hint) : hint(hint) {
|
||||||
|
}
|
||||||
|
void OnDispatch(FiberHandle fiber_handle) override {
|
||||||
|
fiber_handle.Schedule(hint);
|
||||||
|
}
|
||||||
|
executors::Hint hint;
|
||||||
|
};
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
static twist::util::ThreadLocalPtr<Fiber> fiber;
|
||||||
|
|
||||||
|
Fiber::Fiber(Scheduler& scheduler, Routine co)
|
||||||
|
: scheduler_(scheduler),
|
||||||
|
stack_(AllocateStack()),
|
||||||
|
coroutine_(std::move(co), stack_.View()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Fiber::Schedule(executors::Hint hint) {
|
||||||
|
scheduler_.Execute(static_cast<FiberTask*>(this), hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Fiber::Yield(executors::Hint hint) {
|
||||||
|
YieldAwaiter awaiter(hint);
|
||||||
|
awaiter_ = &awaiter;
|
||||||
|
coroutine_.Suspend();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Fiber::Step() {
|
||||||
|
Fiber* tmp = fiber.Exchange(this);
|
||||||
|
coroutine_.Resume();
|
||||||
|
fiber.Exchange(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
FiberHandle Fiber::Dispatch() {
|
||||||
|
if (!coroutine_.IsCompleted()) {
|
||||||
|
auto tmp = awaiter_;
|
||||||
|
awaiter_ = nullptr;
|
||||||
|
return tmp->TransferTo(FiberHandle(this));
|
||||||
|
//
|
||||||
|
// static_cast<IAwaiter*>(tmp)->OnDispatch(FiberHandle(this));
|
||||||
|
// return FiberHandle::Invalid();
|
||||||
|
} else {
|
||||||
|
delete this;
|
||||||
|
return FiberHandle::Invalid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Fiber& Fiber::Self() {
|
||||||
|
return *fiber;
|
||||||
|
}
|
||||||
|
|
||||||
|
Fiber::~Fiber() {
|
||||||
|
ReleaseStack(std::move(stack_));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Fiber::Suspend(IContinuationStealingAwaiter* awaiter) {
|
||||||
|
awaiter_ = awaiter;
|
||||||
|
coroutine_.Suspend();
|
||||||
|
}
|
||||||
|
|
||||||
|
FiberHandle Fiber::Resume() {
|
||||||
|
Step();
|
||||||
|
return Dispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Scheduler& Fiber::GetScheduler() {
|
||||||
|
return scheduler_;
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// API Implementation
|
||||||
|
|
||||||
|
void Go(Scheduler& scheduler, Routine routine) {
|
||||||
|
Fiber* f = new Fiber(scheduler, std::move(routine));
|
||||||
|
f->Schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Go(Routine routine) {
|
||||||
|
Go(Fiber::Self().GetScheduler(), std::move(routine));
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace self {
|
||||||
|
|
||||||
|
void Yield(executors::Hint hint) {
|
||||||
|
Fiber::Self().Yield(hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Suspend(exe::fibers::IContinuationStealingAwaiter* awaiter) {
|
||||||
|
Fiber::Self().Suspend(awaiter);
|
||||||
|
}
|
||||||
|
} // namespace self
|
||||||
|
|
||||||
|
} // namespace exe::fibers
|
52
fibers/scheduler/exe/fibers/core/fiber.hpp
Normal file
52
fibers/scheduler/exe/fibers/core/fiber.hpp
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/fibers/core/api.hpp>
|
||||||
|
#include <exe/coroutine/impl.hpp>
|
||||||
|
#include <context/stack.hpp>
|
||||||
|
#include <exe/fibers/core/awaiter.hpp>
|
||||||
|
|
||||||
|
#include <exe/executors/task.hpp> // experiment
|
||||||
|
|
||||||
|
enum FiberState { Running, Suspend, Waiting };
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
struct FiberTask : public executors::TaskBase {
|
||||||
|
virtual void Run() noexcept override;
|
||||||
|
virtual void Discard() noexcept override;
|
||||||
|
};
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
// Fiber = Stackful coroutine + Scheduler (Thread pool)
|
||||||
|
|
||||||
|
class Fiber : public detail::FiberTask {
|
||||||
|
friend detail::FiberTask;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Fiber(Scheduler& scheduler, Routine co);
|
||||||
|
|
||||||
|
void Schedule(executors::Hint hint = executors::Hint::UpToYou);
|
||||||
|
|
||||||
|
void Yield(executors::Hint hint);
|
||||||
|
|
||||||
|
FiberHandle Dispatch();
|
||||||
|
void Suspend(IContinuationStealingAwaiter*);
|
||||||
|
FiberHandle Resume();
|
||||||
|
Scheduler& GetScheduler();
|
||||||
|
|
||||||
|
static Fiber& Self();
|
||||||
|
~Fiber();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Task
|
||||||
|
void Step();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Scheduler& scheduler_;
|
||||||
|
context::Stack stack_;
|
||||||
|
exe::coroutine::CoroutineImpl coroutine_;
|
||||||
|
IContinuationStealingAwaiter* awaiter_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::fibers
|
27
fibers/scheduler/exe/fibers/core/handle.cpp
Normal file
27
fibers/scheduler/exe/fibers/core/handle.cpp
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#include <exe/fibers/core/handle.hpp>
|
||||||
|
|
||||||
|
#include <exe/fibers/core/fiber.hpp>
|
||||||
|
|
||||||
|
#include <wheels/support/assert.hpp>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
|
||||||
|
Fiber* FiberHandle::Release() {
|
||||||
|
WHEELS_ASSERT(fiber_ != nullptr, "Invalid fiber handle");
|
||||||
|
return std::exchange(fiber_, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FiberHandle::Schedule(executors::Hint hint) {
|
||||||
|
Release()->Schedule(hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FiberHandle::Resume() {
|
||||||
|
auto fh = Release()->Resume();
|
||||||
|
while (fh.IsValid()) {
|
||||||
|
fh = fh.Release()->Resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace exe::fibers
|
41
fibers/scheduler/exe/fibers/core/handle.hpp
Normal file
41
fibers/scheduler/exe/fibers/core/handle.hpp
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
#pragma once
|
||||||
|
#include <exe/executors/executor.hpp>
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
|
||||||
|
class Fiber;
|
||||||
|
|
||||||
|
// Lightweight non-owning handle to a _suspended_ fiber object
|
||||||
|
|
||||||
|
class FiberHandle {
|
||||||
|
friend class Fiber;
|
||||||
|
|
||||||
|
public:
|
||||||
|
FiberHandle() : FiberHandle(nullptr) {
|
||||||
|
}
|
||||||
|
|
||||||
|
static FiberHandle Invalid() {
|
||||||
|
return FiberHandle(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsValid() const {
|
||||||
|
return fiber_ != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule to an associated scheduler
|
||||||
|
void Schedule(executors::Hint hint = executors::Hint::UpToYou);
|
||||||
|
|
||||||
|
// Resume immediately in the current thread
|
||||||
|
void Resume();
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit FiberHandle(Fiber* fiber) : fiber_(fiber) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Fiber* Release();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Fiber* fiber_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::fibers
|
54
fibers/scheduler/exe/fibers/core/stacks.cpp
Normal file
54
fibers/scheduler/exe/fibers/core/stacks.cpp
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
#include <exe/fibers/core/stacks.hpp>
|
||||||
|
#include <deque>
|
||||||
|
#include <exe/support/spinlock.hpp>
|
||||||
|
|
||||||
|
using context::Stack;
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
class StackAllocator {
|
||||||
|
public:
|
||||||
|
Stack Allocate() {
|
||||||
|
{
|
||||||
|
std::lock_guard guard(guard_stacks_);
|
||||||
|
if (!stacks_.empty()) {
|
||||||
|
auto stack = std::move(stacks_.back());
|
||||||
|
stacks_.pop_back();
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AllocateNewStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Release(Stack stack) {
|
||||||
|
std::lock_guard guard(guard_stacks_);
|
||||||
|
stacks_.push_back(std::move(stack));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static Stack AllocateNewStack() {
|
||||||
|
static const size_t kStackPages = 16; // 16 * 4KB = 64KB
|
||||||
|
return Stack::AllocatePages(kStackPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::deque<Stack> stacks_;
|
||||||
|
support::SpinLock guard_stacks_;
|
||||||
|
};
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
StackAllocator allocator;
|
||||||
|
|
||||||
|
context::Stack AllocateStack() {
|
||||||
|
return allocator.Allocate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReleaseStack(context::Stack stack) {
|
||||||
|
allocator.Release(std::move(stack));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace exe::fibers
|
10
fibers/scheduler/exe/fibers/core/stacks.hpp
Normal file
10
fibers/scheduler/exe/fibers/core/stacks.hpp
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <context/stack.hpp>
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
|
||||||
|
context::Stack AllocateStack();
|
||||||
|
void ReleaseStack(context::Stack stack);
|
||||||
|
|
||||||
|
} // namespace exe::fibers
|
41
fibers/scheduler/exe/fibers/sync/condvar.hpp
Normal file
41
fibers/scheduler/exe/fibers/sync/condvar.hpp
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/fibers/sync/mutex.hpp>
|
||||||
|
#include <exe/fibers/sync/futex.hpp>
|
||||||
|
|
||||||
|
#include <twist/stdlike/atomic.hpp>
|
||||||
|
|
||||||
|
// std::unique_lock
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
|
||||||
|
class CondVar {
|
||||||
|
using Lock = std::unique_lock<Mutex>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void Wait(Lock& lock) {
|
||||||
|
uint32_t old = count_notifies_.load();
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
notifier_.ParkIfEqual(old);
|
||||||
|
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotifyOne() {
|
||||||
|
count_notifies_.fetch_add(1);
|
||||||
|
notifier_.WakeOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotifyAll() {
|
||||||
|
count_notifies_.fetch_add(1);
|
||||||
|
notifier_.WakeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
twist::stdlike::atomic<uint32_t> count_notifies_{0};
|
||||||
|
FutexLike<uint32_t> notifier_{count_notifies_};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::fibers
|
103
fibers/scheduler/exe/fibers/sync/futex.hpp
Normal file
103
fibers/scheduler/exe/fibers/sync/futex.hpp
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/fibers/core/api.hpp>
|
||||||
|
|
||||||
|
#include <twist/stdlike/atomic.hpp>
|
||||||
|
|
||||||
|
#include <wheels/intrusive/list.hpp>
|
||||||
|
#include <wheels/intrusive/forward_list.hpp>
|
||||||
|
#include <wheels/support/assert.hpp>
|
||||||
|
|
||||||
|
#include <exe/support/spinlock.hpp>
|
||||||
|
|
||||||
|
#include <exe/fibers/core/fiber.hpp>
|
||||||
|
#include <wheels/logging/logging.hpp>
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept Unlockable = requires(T x) {
|
||||||
|
x.Unlock();
|
||||||
|
};
|
||||||
|
|
||||||
|
template <Unlockable T>
|
||||||
|
class FutexAwaiter : public IAwaiter,
|
||||||
|
public wheels::IntrusiveForwardListNode<FutexAwaiter<T>> {
|
||||||
|
public:
|
||||||
|
explicit FutexAwaiter(T& value) : value_(value) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDispatch(FiberHandle fiber_handle) override {
|
||||||
|
fiber_handle_ = fiber_handle;
|
||||||
|
value_.Unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Schedule() {
|
||||||
|
fiber_handle_.Schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
T& value_;
|
||||||
|
FiberHandle fiber_handle_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class FutexLike {
|
||||||
|
public:
|
||||||
|
explicit FutexLike(twist::stdlike::atomic<T>& cell) : cell_(cell) {
|
||||||
|
}
|
||||||
|
|
||||||
|
~FutexLike() {
|
||||||
|
if (!sleeping_queue_.IsEmpty()) {
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Park current fiber if cell.load() == `old`
|
||||||
|
void ParkIfEqual(T old) {
|
||||||
|
guard_sleep_queue_.lock();
|
||||||
|
if (cell_.load() != old) {
|
||||||
|
guard_sleep_queue_.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto awaiter = FutexAwaiter(guard_sleep_queue_);
|
||||||
|
|
||||||
|
sleeping_queue_.PushBack(&awaiter);
|
||||||
|
|
||||||
|
self::Suspend(&awaiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WakeOne() {
|
||||||
|
std::lock_guard g(guard_sleep_queue_);
|
||||||
|
|
||||||
|
if (sleeping_queue_.IsEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleeping_queue_.PopFront()->Schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WakeAll() {
|
||||||
|
std::unique_lock lock(guard_sleep_queue_);
|
||||||
|
|
||||||
|
if (sleeping_queue_.IsEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto wakeup_queue = std::move(sleeping_queue_);
|
||||||
|
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
while (!wakeup_queue.IsEmpty()) {
|
||||||
|
wakeup_queue.PopFront()->Schedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
exe::support::SpinLock guard_sleep_queue_;
|
||||||
|
twist::stdlike::atomic<T>& cell_;
|
||||||
|
|
||||||
|
wheels::IntrusiveForwardList<FutexAwaiter<support::SpinLock>> sleeping_queue_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::fibers
|
57
fibers/scheduler/exe/fibers/sync/mutex.hpp
Normal file
57
fibers/scheduler/exe/fibers/sync/mutex.hpp
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/fibers/sync/futex.hpp>
|
||||||
|
|
||||||
|
#include <twist/stdlike/atomic.hpp>
|
||||||
|
#include <exe/support/lockfree_mutex_queue.hpp>
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
|
||||||
|
class Mutex {
|
||||||
|
struct Node : public detail::mutex::IntrusiveNode {
|
||||||
|
FiberHandle fiber_handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MutexAwaiter : public IAwaiter {
|
||||||
|
explicit MutexAwaiter(Mutex& mutex) : mutex(mutex) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDispatch(FiberHandle fiber_handle) override {
|
||||||
|
node.fiber_handle = fiber_handle;
|
||||||
|
if (mutex.awaiters_.TryLockOrEnqueue(&node)) {
|
||||||
|
node.fiber_handle.Schedule(executors::Hint::Next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mutex& mutex;
|
||||||
|
Node node;
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
void Lock() {
|
||||||
|
if (awaiters_.TryLock()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MutexAwaiter mutex_awaiter(*this);
|
||||||
|
fibers::self::Suspend(&mutex_awaiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Unlock() {
|
||||||
|
auto node = awaiters_.TryPopOrUnlock();
|
||||||
|
if (node != nullptr) {
|
||||||
|
auto awaiter = fibers::ContinuationStealingAwaiter(node->fiber_handle);
|
||||||
|
fibers::self::Suspend(&awaiter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void lock() { // NOLINT
|
||||||
|
Lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void unlock() { // NOLINT
|
||||||
|
Unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
detail::mutex::LockfreeIntrusiveQueue<Node> awaiters_;
|
||||||
|
};
|
||||||
|
} // namespace exe::fibers
|
57
fibers/scheduler/exe/fibers/sync/wait_group.hpp
Normal file
57
fibers/scheduler/exe/fibers/sync/wait_group.hpp
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exe/fibers/sync/futex.hpp>
|
||||||
|
#include <twist/stdlike/atomic.hpp>
|
||||||
|
|
||||||
|
namespace exe::fibers {
|
||||||
|
|
||||||
|
// https://gobyexample.com/waitgroups
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
class WaitGroupParking {
|
||||||
|
public:
|
||||||
|
void Notify() {
|
||||||
|
done_.store(1);
|
||||||
|
notifier_.WakeAll();
|
||||||
|
done_.store(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Wait() {
|
||||||
|
while (done_.load() == 0) {
|
||||||
|
notifier_.ParkIfEqual(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (done_.load() == 1) {
|
||||||
|
self::Yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// 0 - wait done, 1 - wait end notify, 2 - released
|
||||||
|
twist::stdlike::atomic<uint32_t> done_{0};
|
||||||
|
FutexLike<uint32_t> notifier_{done_};
|
||||||
|
};
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
class WaitGroup {
|
||||||
|
public:
|
||||||
|
void Add(size_t count) {
|
||||||
|
counter_.fetch_add(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Done() {
|
||||||
|
if (counter_.fetch_sub(1) == 1) {
|
||||||
|
releaser_.Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Wait() {
|
||||||
|
releaser_.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
twist::stdlike::atomic<uint32_t> counter_{0};
|
||||||
|
detail::WaitGroupParking releaser_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::fibers
|
108
fibers/scheduler/exe/support/lockfree_mutex_queue.hpp
Normal file
108
fibers/scheduler/exe/support/lockfree_mutex_queue.hpp
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <twist/stdlike/atomic.hpp>
|
||||||
|
|
||||||
|
namespace exe::fibers::detail::mutex {
|
||||||
|
|
||||||
|
struct IntrusiveNode {
|
||||||
|
uintptr_t next = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class LockfreeIntrusiveQueue {
|
||||||
|
using Node = IntrusiveNode;
|
||||||
|
|
||||||
|
enum State : uintptr_t { kUnlock = 1, kLock = 0 };
|
||||||
|
|
||||||
|
public:
|
||||||
|
// if tail == kUnlock then try cas to kLock
|
||||||
|
// else try push
|
||||||
|
// return true if successful lock
|
||||||
|
bool TryLockOrEnqueue(Node* new_node) {
|
||||||
|
new_node->next = tail_.load();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (new_node->next == State::kUnlock) {
|
||||||
|
if (tail_.compare_exchange_weak(new_node->next, State::kLock)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (tail_.compare_exchange_weak(new_node->next, ToUIntPtr(new_node))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
T* TryPopOrUnlock() {
|
||||||
|
if (output_ == nullptr) {
|
||||||
|
auto output = GrabOrUnlock();
|
||||||
|
if (output == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
output_ = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PopFromOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
T* PopFromOutput() {
|
||||||
|
return static_cast<T*>(std::exchange(output_, ToNode(output_->next)));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TryLock() {
|
||||||
|
auto old = tail_.load();
|
||||||
|
if (old == State::kUnlock) {
|
||||||
|
return tail_.compare_exchange_weak(old, State::kLock);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node* GrabOrUnlock() {
|
||||||
|
uintptr_t old = tail_.load();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (old == State::kLock) {
|
||||||
|
if (tail_.compare_exchange_weak(old, State::kUnlock)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ReverseStack(tail_.exchange(State::kLock));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static uintptr_t ToUIntPtr(Node* node) {
|
||||||
|
return reinterpret_cast<uintptr_t>(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Node* ToNode(uintptr_t ptr) {
|
||||||
|
return reinterpret_cast<Node*>(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Node* ReverseStack(uintptr_t ptr) {
|
||||||
|
if (ptr == State::kUnlock || ptr == State::kLock) {
|
||||||
|
WHEELS_PANIC("reverse stack");
|
||||||
|
}
|
||||||
|
uintptr_t prev = ptr;
|
||||||
|
ptr = ToNode(ptr)->next;
|
||||||
|
ToNode(prev)->next = State::kLock;
|
||||||
|
|
||||||
|
while (ptr != State::kLock) {
|
||||||
|
uintptr_t next = ToNode(ptr)->next;
|
||||||
|
ToNode(ptr)->next = prev;
|
||||||
|
prev = ptr;
|
||||||
|
ptr = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToNode(prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
twist::stdlike::atomic<uintptr_t> tail_{State::kUnlock};
|
||||||
|
Node* output_ = nullptr;
|
||||||
|
};
|
||||||
|
} // namespace exe::fibers::detail::mutex
|
41
fibers/scheduler/exe/support/spinlock.hpp
Normal file
41
fibers/scheduler/exe/support/spinlock.hpp
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <twist/stdlike/atomic.hpp>
|
||||||
|
#include <twist/util/spin_wait.hpp>
|
||||||
|
#include <wheels/logging/logging.hpp>
|
||||||
|
#include <twist/util/spin_wait.hpp>
|
||||||
|
|
||||||
|
namespace exe::support {
|
||||||
|
|
||||||
|
// Test-and-TAS spinlock
|
||||||
|
|
||||||
|
class SpinLock {
|
||||||
|
public:
|
||||||
|
void Lock() {
|
||||||
|
while (is_locked_.exchange(true, std::memory_order::acquire)) {
|
||||||
|
twist::util::SpinWait s;
|
||||||
|
while (is_locked_.load(std::memory_order::relaxed)) {
|
||||||
|
s.Spin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Unlock() {
|
||||||
|
is_locked_.store(false, std::memory_order::release);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BasicLockable
|
||||||
|
|
||||||
|
void lock() { // NOLINT
|
||||||
|
Lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void unlock() { // NOLINT
|
||||||
|
Unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
twist::stdlike::atomic<bool> is_locked_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace exe::support
|
36
fibers/scheduler/exe/support/wait_group.hpp
Normal file
36
fibers/scheduler/exe/support/wait_group.hpp
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <twist/stdlike/atomic.hpp>
|
||||||
|
#include <wheels/logging/logging.hpp>
|
||||||
|
|
||||||
|
namespace exe::support {
|
||||||
|
class WaitGroup {
|
||||||
|
public:
|
||||||
|
explicit WaitGroup(uint32_t count = 0) : counter_(count) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Add() {
|
||||||
|
zero_notifier_.store(0);
|
||||||
|
counter_.fetch_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Done() {
|
||||||
|
if (counter_.fetch_sub(1) == 1) {
|
||||||
|
zero_notifier_.fetch_add(1);
|
||||||
|
zero_notifier_.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Wait() {
|
||||||
|
uint32_t old_zero_notifier = 0;
|
||||||
|
while (counter_.load() != 0) {
|
||||||
|
zero_notifier_.wait(old_zero_notifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
twist::stdlike::atomic<uint32_t> counter_{0};
|
||||||
|
twist::stdlike::atomic<uint32_t> zero_notifier_{0};
|
||||||
|
};
|
||||||
|
} // namespace exe::support
|
Loading…
Reference in a new issue