Skip to content

Commit 0aa5709

Browse files
committed
Optimize ASU task manager with lock-free slots
1 parent 32513bf commit 0aa5709

2 files changed

Lines changed: 923 additions & 26 deletions

File tree

ucm/transport/kv/asu/common/task_manager_base.h

Lines changed: 250 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,50 @@
2323
* */
2424
#pragma once
2525

26+
#include <algorithm>
2627
#include <atomic>
28+
#include <cstddef>
29+
#include <cstdint>
30+
#include <limits>
2731
#include <memory>
28-
#include <mutex>
2932
#include <string>
30-
#include <unordered_map>
33+
#include <type_traits>
3134
#include <utility>
35+
#include <vector>
3236
#include "asu_transport/types.h"
3337

3438
namespace UC::ASU {
3539

3640
template <typename Context, typename State>
3741
class TaskManagerBase {
3842
public:
39-
TaskManagerBase(State initialState, std::string taskName)
40-
: initialState_(initialState), taskName_(std::move(taskName))
43+
static constexpr std::size_t kMinSlotCount = 1024;
44+
static constexpr std::size_t kDefaultMaxInflightTasks = 4096;
45+
46+
static std::size_t RecommendSlotCount(std::size_t maxInflightTasks)
47+
{
48+
// Reserve extra slots for the configured maximum inflight workload.
49+
// For example: 4096 inflight tasks -> 8192 slots.
50+
const auto required = std::max<std::size_t>(kMinSlotCount, maxInflightTasks * 2);
51+
return NormalizeSlotCount(required);
52+
}
53+
54+
explicit TaskManagerBase(State initialState, std::string taskName,
55+
std::size_t maxInflightTasks = kDefaultMaxInflightTasks)
56+
: initialState_(initialState),
57+
taskName_(std::move(taskName)),
58+
slotIndexBits_(ComputeSlotIndexBits(RecommendSlotCount(maxInflightTasks))),
59+
slots_(RecommendSlotCount(maxInflightTasks)),
60+
slotMask_(slots_.size() - 1),
61+
freeListShift_(slotIndexBits_ + 1),
62+
freeListMask_(MakeLowBitsMask(slotIndexBits_ + 1)),
63+
freeListEnd_(slots_.size()),
64+
freeListHead_(PackFreeListHead(0, 0))
4165
{
66+
for (std::size_t i = 0; i + 1 < slots_.size(); ++i) {
67+
slots_[i].freeNext.store(i + 1, std::memory_order_relaxed);
68+
}
69+
slots_[slots_.size() - 1].freeNext.store(freeListEnd_, std::memory_order_relaxed);
4270
}
4371

4472
Status Submit(std::unique_ptr<Context> ctx, TaskId& taskId)
@@ -48,53 +76,249 @@ class TaskManagerBase {
4876
return Status::Error(StatusCode::INVALID_ARGUMENT, taskName_ + " task context is null");
4977
}
5078

79+
auto slotIndex = FreeListPop();
80+
if (slotIndex == freeListEnd_) {
81+
taskId = kInvalidTaskId;
82+
return Status::Error(StatusCode::INTERNAL_ERROR, taskName_ + " task table is full");
83+
}
84+
85+
auto& slot = slots_[slotIndex];
86+
87+
std::uint8_t expected = SlotState::EMPTY;
88+
if (!slot.state.compare_exchange_strong(expected, SlotState::WRITING,
89+
std::memory_order_acq_rel,
90+
std::memory_order_acquire)) {
91+
FreeListPush(slotIndex);
92+
taskId = kInvalidTaskId;
93+
return Status::Error(StatusCode::INTERNAL_ERROR, taskName_ + " task slot not empty");
94+
}
95+
96+
const auto generation = slot.generation.fetch_add(1, std::memory_order_relaxed) + 1;
97+
if (!CanEncodeGeneration(generation)) {
98+
AtomicStoreCtx(slot, std::shared_ptr<Context>{}, std::memory_order_release);
99+
slot.taskId.store(kInvalidTaskId, std::memory_order_release);
100+
slot.state.store(SlotState::EMPTY, std::memory_order_release);
101+
FreeListPush(slotIndex);
102+
taskId = kInvalidTaskId;
103+
return Status::Error(StatusCode::INTERNAL_ERROR,
104+
taskName_ + " task id generation overflow");
105+
}
106+
107+
const auto newTaskId = MakeTaskId(slotIndex, generation);
108+
ctx->state.store(initialState_, std::memory_order_release);
109+
ctx->taskId = newTaskId;
51110
auto sharedCtx = std::shared_ptr<Context>(std::move(ctx));
52-
sharedCtx->state.store(initialState_, std::memory_order_release);
53111

54-
std::lock_guard<std::mutex> lock(mutex_);
55-
do {
56-
taskId = nextTaskId_.fetch_add(1, std::memory_order_relaxed);
57-
} while (taskId == kInvalidTaskId || tasks_.find(taskId) != tasks_.end());
112+
AtomicStoreCtx(slot, sharedCtx, std::memory_order_release);
113+
slot.taskId.store(newTaskId, std::memory_order_release);
114+
slot.state.store(SlotState::READY, std::memory_order_release);
58115

59-
sharedCtx->taskId = taskId;
60-
tasks_.emplace(taskId, std::move(sharedCtx));
116+
taskId = newTaskId;
61117
return Status::OK();
62118
}
63119

64120
std::shared_ptr<Context> Get(TaskId taskId)
65121
{
66-
std::lock_guard<std::mutex> lock(mutex_);
67-
auto iter = tasks_.find(taskId);
68-
if (iter == tasks_.end()) { return nullptr; }
69-
return iter->second;
122+
if (taskId == kInvalidTaskId) { return nullptr; }
123+
124+
const auto slotIndex = static_cast<std::size_t>(ToTaskIdUInt(taskId) & slotMask_);
125+
auto& slot = slots_[slotIndex];
126+
127+
const auto state1 = slot.state.load(std::memory_order_acquire);
128+
if (state1 != SlotState::READY) { return nullptr; }
129+
130+
const auto id1 = slot.taskId.load(std::memory_order_acquire);
131+
if (id1 != taskId) { return nullptr; }
132+
133+
auto ptr = AtomicLoadCtx(slot, std::memory_order_acquire);
134+
if (!ptr) { return nullptr; }
135+
136+
const auto id2 = slot.taskId.load(std::memory_order_acquire);
137+
const auto state2 = slot.state.load(std::memory_order_acquire);
138+
if (state2 == SlotState::READY && id2 == taskId && ptr->taskId == taskId) { return ptr; }
139+
140+
return nullptr;
70141
}
71142

72143
std::vector<std::shared_ptr<Context>> GetAll()
73144
{
74-
std::lock_guard<std::mutex> lock(mutex_);
75145
std::vector<std::shared_ptr<Context>> tasks;
76-
tasks.reserve(tasks_.size());
77-
for (const auto& item : tasks_) { tasks.emplace_back(item.second); }
146+
for (const auto& slot : slots_) {
147+
if (slot.state.load(std::memory_order_acquire) != SlotState::READY) { continue; }
148+
149+
auto ctx = AtomicLoadCtx(slot, std::memory_order_acquire);
150+
if (!ctx) { continue; }
151+
152+
const auto taskId = slot.taskId.load(std::memory_order_acquire);
153+
const auto state = slot.state.load(std::memory_order_acquire);
154+
if (state == SlotState::READY && taskId == ctx->taskId) {
155+
tasks.emplace_back(std::move(ctx));
156+
}
157+
}
78158
return tasks;
79159
}
80160

81161
Status Remove(TaskId taskId)
82162
{
83-
std::lock_guard<std::mutex> lock(mutex_);
84-
auto erased = tasks_.erase(taskId);
85-
if (erased == 0) {
163+
if (taskId == kInvalidTaskId) {
164+
return Status::Error(StatusCode::TASK_NOT_FOUND, taskName_ + " task not found");
165+
}
166+
167+
const auto slotIndex = static_cast<std::size_t>(ToTaskIdUInt(taskId) & slotMask_);
168+
auto& slot = slots_[slotIndex];
169+
170+
std::uint8_t expected = SlotState::READY;
171+
if (!slot.state.compare_exchange_strong(expected, SlotState::REMOVING,
172+
std::memory_order_acq_rel,
173+
std::memory_order_acquire)) {
174+
return Status::Error(StatusCode::TASK_NOT_FOUND, taskName_ + " task not found");
175+
}
176+
177+
if (slot.taskId.load(std::memory_order_acquire) != taskId) {
178+
slot.state.store(SlotState::READY, std::memory_order_release);
86179
return Status::Error(StatusCode::TASK_NOT_FOUND, taskName_ + " task not found");
87180
}
181+
182+
AtomicStoreCtx(slot, std::shared_ptr<Context>{}, std::memory_order_release);
183+
slot.taskId.store(kInvalidTaskId, std::memory_order_release);
184+
slot.state.store(SlotState::EMPTY, std::memory_order_release);
185+
186+
FreeListPush(slotIndex);
88187
return Status::OK();
89188
}
90189

91-
protected:
190+
private:
191+
using TaskIdUInt = std::make_unsigned_t<TaskId>;
192+
193+
struct SlotState {
194+
static constexpr std::uint8_t EMPTY = 0;
195+
static constexpr std::uint8_t WRITING = 1;
196+
static constexpr std::uint8_t READY = 2;
197+
static constexpr std::uint8_t REMOVING = 3;
198+
};
199+
200+
struct alignas(64) Slot {
201+
std::atomic<std::uint8_t> state{SlotState::EMPTY};
202+
std::atomic<TaskIdUInt> generation{0};
203+
std::atomic<TaskId> taskId{kInvalidTaskId};
204+
std::shared_ptr<Context> ctx;
205+
std::atomic<std::size_t> freeNext{0};
206+
};
207+
208+
private:
209+
static TaskIdUInt ToTaskIdUInt(TaskId taskId) { return static_cast<TaskIdUInt>(taskId); }
210+
211+
static std::size_t NormalizeSlotCount(std::size_t n)
212+
{
213+
n = std::max<std::size_t>(n, kMinSlotCount);
214+
215+
std::size_t power = 1;
216+
while (power < n) {
217+
if (power > (std::numeric_limits<std::size_t>::max() >> 1)) { return power; }
218+
power <<= 1;
219+
}
220+
221+
return power;
222+
}
223+
224+
static std::size_t ComputeSlotIndexBits(std::size_t slotCount)
225+
{
226+
std::size_t bits = 0;
227+
for (auto s = slotCount; s > 1; s >>= 1) { ++bits; }
228+
return bits;
229+
}
230+
231+
static std::uint64_t MakeLowBitsMask(std::size_t bits)
232+
{
233+
if (bits >= std::numeric_limits<std::uint64_t>::digits) {
234+
return std::numeric_limits<std::uint64_t>::max();
235+
}
236+
return (1ULL << bits) - 1;
237+
}
238+
239+
bool CanEncodeGeneration(TaskIdUInt generation) const
240+
{
241+
constexpr std::size_t kTotalBits =
242+
static_cast<std::size_t>(std::numeric_limits<TaskIdUInt>::digits);
243+
244+
if (slotIndexBits_ >= kTotalBits) { return false; }
245+
246+
if (generation == 0) { return false; }
247+
248+
const auto generationBits = kTotalBits - slotIndexBits_;
249+
if (generationBits >= kTotalBits) { return true; }
250+
251+
const auto maxGeneration = (static_cast<TaskIdUInt>(1) << generationBits) - 1;
252+
return generation <= maxGeneration;
253+
}
254+
255+
TaskId MakeTaskId(std::size_t slotIndex, TaskIdUInt generation) const
256+
{
257+
const auto raw =
258+
(generation << slotIndexBits_) | static_cast<TaskIdUInt>(slotIndex & slotMask_);
259+
return static_cast<TaskId>(raw);
260+
}
261+
262+
static std::shared_ptr<Context> AtomicLoadCtx(const Slot& slot, std::memory_order order)
263+
{
264+
return std::atomic_load_explicit(&slot.ctx, order);
265+
}
266+
267+
static void AtomicStoreCtx(Slot& slot, std::shared_ptr<Context> ptr, std::memory_order order)
268+
{
269+
std::atomic_store_explicit(&slot.ctx, std::move(ptr), order);
270+
}
271+
272+
std::uint64_t PackFreeListHead(std::uint64_t generation, std::size_t index) const
273+
{
274+
return (generation << freeListShift_) | static_cast<std::uint64_t>(index);
275+
}
276+
277+
std::size_t FreeListPop()
278+
{
279+
auto oldHead = freeListHead_.load(std::memory_order_acquire);
280+
while (true) {
281+
const auto index = static_cast<std::size_t>(oldHead & freeListMask_);
282+
if (index == freeListEnd_) { return freeListEnd_; }
283+
284+
const auto nextIndex = slots_[index].freeNext.load(std::memory_order_acquire);
285+
const auto oldGen = oldHead >> freeListShift_;
286+
const auto newHead = PackFreeListHead(oldGen + 1, nextIndex);
287+
if (freeListHead_.compare_exchange_weak(oldHead, newHead, std::memory_order_acq_rel,
288+
std::memory_order_acquire)) {
289+
return index;
290+
}
291+
}
292+
}
293+
294+
void FreeListPush(std::size_t slotIndex)
295+
{
296+
auto oldHead = freeListHead_.load(std::memory_order_acquire);
297+
while (true) {
298+
slots_[slotIndex].freeNext.store(static_cast<std::size_t>(oldHead & freeListMask_),
299+
std::memory_order_release);
300+
301+
const auto oldGen = oldHead >> freeListShift_;
302+
const auto newHead = PackFreeListHead(oldGen + 1, slotIndex);
303+
if (freeListHead_.compare_exchange_weak(oldHead, newHead, std::memory_order_release,
304+
std::memory_order_acquire)) {
305+
return;
306+
}
307+
}
308+
}
309+
310+
private:
92311
State initialState_;
93312
std::string taskName_;
94-
std::atomic<TaskId> nextTaskId_{1};
95-
// TODO: consider using a lock-free structure !
96-
std::mutex mutex_;
97-
std::unordered_map<TaskId, std::shared_ptr<Context>> tasks_;
313+
std::size_t slotIndexBits_{0};
314+
315+
std::vector<Slot> slots_;
316+
std::size_t slotMask_{0};
317+
318+
std::size_t freeListShift_{0};
319+
std::uint64_t freeListMask_{0};
320+
std::size_t freeListEnd_{0};
321+
std::atomic<std::uint64_t> freeListHead_{0};
98322
};
99323

100324
} // namespace UC::ASU

0 commit comments

Comments
 (0)