Skip to content

Commit 6804d27

Browse files
committed
more design enhancements, docs
1 parent c4b5d90 commit 6804d27

File tree

3 files changed

+180
-46
lines changed

3 files changed

+180
-46
lines changed

llvm/include/llvm/ExecutionEngine/Orc/TaskDispatch.h

Lines changed: 130 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#define LLVM_EXECUTIONENGINE_ORC_TASKDISPATCH_H
1515

1616
#include "llvm/Config/llvm-config.h"
17+
#include "llvm/ADT/BitmaskEnum.h"
1718
#include "llvm/Support/Compiler.h"
1819
#include "llvm/Support/Debug.h"
1920
#include "llvm/Support/ErrorHandling.h"
@@ -115,7 +116,7 @@ class LLVM_ABI TaskDispatcher {
115116
public:
116117
virtual ~TaskDispatcher();
117118

118-
/// Run the given task.
119+
/// Schedule the given task to run.
119120
virtual void dispatch(std::unique_ptr<Task> T) = 0;
120121

121122
/// Called by ExecutionSession. Waits until all tasks have completed.
@@ -125,15 +126,21 @@ class LLVM_ABI TaskDispatcher {
125126
virtual void work_until(future_base &F) = 0;
126127
};
127128

128-
/// Runs all tasks on the current thread.
129+
/// Runs all tasks on the current thread, at the next work_until yield point.
129130
class LLVM_ABI InPlaceTaskDispatcher : public TaskDispatcher {
130131
public:
131132
void dispatch(std::unique_ptr<Task> T) override;
132133
void shutdown() override;
133134
void work_until(future_base &F) override;
134135

135136
private:
136-
thread_local static SmallVector<std::unique_ptr<Task>> TaskQueue;
137+
/// C++ does not support non-static thread_local variables, so this needs to
138+
/// store both the task and the associated dispatcher queue so that shutdown
139+
/// can wait for the correct tasks to finish.
140+
#if LLVM_ENABLE_THREADS
141+
thread_local static
142+
#endif
143+
SmallVector<std::pair<std::unique_ptr<Task>, InPlaceTaskDispatcher*>> TaskQueue;
137144

138145
#if LLVM_ENABLE_THREADS
139146
std::mutex DispatchMutex;
@@ -172,32 +179,55 @@ class LLVM_ABI DynamicThreadPoolTaskDispatcher : public TaskDispatcher {
172179

173180
#endif // LLVM_ENABLE_THREADS
174181

175-
/// ORC-TaskDispatch aware promise/future class that can help with task dispatch while waiting
176-
// TODO: docs
182+
/// @name ORC Promise/Future Classes
183+
///
184+
/// ORC-aware promise/future implementation that integrates with the
185+
/// TaskDispatcher system to allow efficient cooperative multitasking while
186+
/// waiting for results (with certain limitations on what can be awaited).
187+
/// Together they provide building blocks for a full async/await-like runtime
188+
/// for llvm that supports multiple threads.
189+
///
190+
/// Unlike std::promise/std::future alone, these classes can help dispatch other
191+
/// tasks while waiting, preventing deadlocks and improving overall system
192+
/// throughput. They have a similar API, though with some important differences
193+
/// and some features simply not currently implemented.
194+
///
195+
/// @{
177196

178197
/// Status for future/promise state
179-
enum class FutureStatus : uint8_t { NotReady = 0, Ready = 1, NotValid = 2 };
198+
enum class FutureStatus : uint8_t { NotReady = 0, Ready = 1 };
199+
200+
/// Status for promise state tracking
201+
enum class PromiseStatus : uint8_t {
202+
Initial = 0,
203+
FutureCreated = 1,
204+
ValueSet = 2,
205+
LLVM_MARK_AS_BITMASK_ENUM(ValueSet)
206+
};
180207

181-
/// Type-erased base class for futures
208+
/// Type-erased base class for futures, generally for scheduler use to avoid
209+
/// needing virtual dispatches
182210
class future_base {
183211
public:
212+
/// Check if the future is now ready with a value (precondition: should be valid)
184213
bool ready() const {
185214
return state_->status_.load(std::memory_order_acquire) !=
186215
FutureStatus::NotReady;
187216
}
188217

189218
/// Check if the future is in a valid state (not moved-from and not consumed)
190219
bool valid() const {
191-
return state_ && state_->status_.load(std::memory_order_acquire) !=
192-
FutureStatus::NotValid;
220+
return state_ != nullptr;
193221
}
194222

195223
/// Wait for the future to be ready, helping with task dispatch
196224
void wait(TaskDispatcher &D) {
197225
// Keep helping with task dispatch until our future is ready
198-
if (!ready())
226+
if (!ready()) {
199227
D.work_until(*this);
200-
assert(ready());
228+
if (state_->status_.load(std::memory_order_relaxed) != FutureStatus::Ready)
229+
report_fatal_error("work_until() returned without this future being ready");
230+
}
201231
}
202232

203233
protected:
@@ -207,19 +237,22 @@ class future_base {
207237

208238
future_base(state_base *state) : state_(state) {}
209239
future_base() = default;
240+
241+
/// Only allow deleting the future once it is invalid
210242
~future_base() {
211243
if (valid())
212244
report_fatal_error("get() must be called before future destruction");
213-
delete state_;
245+
// state_ is already nullptr if get() was called, otherwise we have an error above
214246
}
215247

216248
// Move constructor and assignment
217249
future_base(future_base &&other) noexcept : state_(other.state_) {
218250
other.state_ = nullptr;
219251
}
252+
220253
future_base &operator=(future_base &&other) noexcept {
221254
if (this != &other) {
222-
delete state_;
255+
this->~future_base();
223256
state_ = other.state_;
224257
other.state_ = nullptr;
225258
}
@@ -229,12 +262,33 @@ class future_base {
229262
state_base *state_;
230263
};
231264

232-
/// ORC-aware future class that can help with task dispatch while waiting
265+
/// TaskDispatcher-aware future class for cooperative await.
266+
///
267+
/// @tparam T The type of value this future will provide. Use void for futures that
268+
/// signal completion without providing a value.
269+
///
270+
/// This future implementation is similar to `std::future`, so most code can
271+
/// transition to it easily`. However, it differs from `std::future` in a few key
272+
/// ways to be aware of:
273+
/// - No exception support (or the overhead for it).
274+
/// - Waiting operations (get(&D), wait(&D)) help dispatch other tasks while
275+
/// blocked, requiring an additional argument of which TaskDispatcher object of
276+
/// where all associated work will be scheduled.
277+
/// - Must call get() exactly once before destruction (enforced with
278+
/// `report_fatal_error`). Internal state is freed when `get` returns.
279+
///
280+
/// Other notable features, in common with `std::future`:
281+
/// - Supports both value types and void specialization through the same interface.
282+
/// - Thread-safe through atomic operations.
283+
/// - Provides acquire-release ordering with `std::promise::set_value()`.
284+
/// - Concurrent access to any method (including to `ready`) on multiple threads
285+
/// is not allowed.
233286

234287
template <typename T> class future;
235288
template <typename T> class promise;
236289
template <typename T> class future : public future_base {
237290
public:
291+
// Template the state struct so that future<void> has no wasted overhead for the value
238292
struct state : public future_base::state_base {
239293
template <typename U> struct value_storage {
240294
U value_;
@@ -254,15 +308,11 @@ template <typename T> class future : public future_base {
254308
future &operator=(future &&) = default;
255309

256310
/// Get the value, helping with task dispatch while waiting.
257-
/// This will destroy the underlying value, so this must only be called once.
311+
/// This will destroy the underlying value, so this must be called exactly once.
258312
T get(TaskDispatcher &D) {
259313
if (!valid())
260314
report_fatal_error("get() must only be called once");
261315
wait(D);
262-
auto old_status = state_->status_.exchange(FutureStatus::NotValid,
263-
std::memory_order_release);
264-
if (old_status != FutureStatus::Ready)
265-
report_fatal_error("get() must only be called once");
266316
return take_value();
267317
}
268318

@@ -271,56 +321,93 @@ template <typename T> class future : public future_base {
271321

272322
template <typename U = T>
273323
typename std::enable_if<!std::is_void<U>::value, U>::type take_value() {
274-
return std::move(
324+
T result = std::move(
275325
static_cast<typename future<T>::state *>(state_)->storage.value_);
326+
delete state_;
327+
state_ = nullptr;
328+
return result;
276329
}
277330

278331
template <typename U = T>
279-
typename std::enable_if<std::is_void<U>::value, U>::type take_value() {}
332+
typename std::enable_if<std::is_void<U>::value, U>::type take_value() {
333+
delete state_;
334+
state_ = nullptr;
335+
}
280336

281337
explicit future(state *state) : future_base(state) {}
282338
};
283339

284-
/// ORC-aware promise class that works with ORC future
340+
/// TaskDispatcher-aware promise class that provides values to associated futures.
341+
///
342+
/// @tparam T The type of value this promise will provide. Use void for promises that
343+
/// signal completion without providing a value.
344+
///
345+
/// This promise implementation provides the value-setting side of the promise/future
346+
/// pair and integrates with the ORC TaskDispatcher system. Key characteristics:
347+
/// - Must call get_future() no more than once to create the associated future.
348+
/// - Must call set_value() exactly once to provide the result.
349+
/// - Automatically cleans up state if get_future() is never called.
350+
/// - Thread-safe from set_value to get.
351+
/// - Move-only semantics to prevent accidental copying.
352+
///
353+
/// @par Error Handling:
354+
/// The promise/future system uses report_fatal_error() for misuse:
355+
/// - Calling get_future() more than once.
356+
/// - Calling set_value() more than once.
357+
/// - Destroying a future without calling get().
358+
/// - Calling get() more than once on a future.
359+
///
360+
/// @par Thread Safety:
361+
/// - Each promise/future must only be accessed by one thread, as concurrent
362+
/// calls to the API functions may result in crashes.
363+
/// - Multiple threads can safely access different promise/future pairs.
364+
/// - set_value() and get() operations are atomic and thread-safe.
365+
/// - Move operations should only be performed by a single thread.
285366
template <typename T> class promise {
286367
friend class future<T>;
287368

288369
public:
289-
promise() : state_(new typename future<T>::state()), future_created_(false) {}
370+
promise() : state_(new typename future<T>::state()), status_(PromiseStatus::Initial) {}
290371

291372
~promise() {
373+
// Assert proper promise lifecycle: ensure set_value was called if
374+
// get_future() was called. This can catch deadlocks where get_future() is
375+
// called but set_value() is never called, though only if the promise is
376+
// moved from instead of borrowed from the frame with the future.
377+
assert((bool)(status_ & PromiseStatus::ValueSet) == (bool)(status_ & PromiseStatus::FutureCreated) &&
378+
"Promise destroyed with mismatched ValueSet and FutureCreated state");
292379
// Delete state only if get_future() was never called
293-
if (!future_created_) {
380+
if (!(status_ & PromiseStatus::FutureCreated)) {
294381
delete state_;
382+
} else {
383+
assert((bool)(status_ & PromiseStatus::ValueSet));
295384
}
296385
}
297386

298387
promise(const promise &) = delete;
299388
promise &operator=(const promise &) = delete;
300389

301390
promise(promise &&other) noexcept
302-
: state_(other.state_), future_created_(other.future_created_) {
391+
: state_(other.state_), status_(other.status_) {
303392
other.state_ = nullptr;
304-
other.future_created_ = false;
393+
other.status_ = PromiseStatus::Initial;
305394
}
306395

307396
promise &operator=(promise &&other) noexcept {
308397
if (this != &other) {
309-
if (!future_created_) {
310-
delete state_;
311-
}
398+
this->~promise();
312399
state_ = other.state_;
313-
future_created_ = other.future_created_;
400+
status_ = other.status_;
314401
other.state_ = nullptr;
315-
other.future_created_ = false;
402+
other.status_ = PromiseStatus::Initial;
316403
}
317404
return *this;
318405
}
319406

320407
/// Get the associated future (must only be called once)
321408
future<T> get_future() {
322-
assert(!future_created_ && "get_future() can only be called once");
323-
future_created_ = true;
409+
assert(!(status_ & PromiseStatus::FutureCreated) && "get_future() can only be called once");
410+
status_ |= PromiseStatus::FutureCreated;
324411
return future<T>(state_);
325412
}
326413

@@ -334,15 +421,19 @@ template <typename T> class promise {
334421
set_value(const typename std::conditional<std::is_void<T>::value,
335422
std::nullopt_t, T>::type &value) {
336423
assert(state_ && "Invalid promise state");
424+
assert(!(status_ & PromiseStatus::ValueSet) && "set_value() can only be called once");
337425
state_->storage.value_ = value;
426+
status_ |= PromiseStatus::ValueSet;
338427
state_->status_.store(FutureStatus::Ready, std::memory_order_release);
339428
}
340429

341430
template <typename U = T>
342431
void set_value(typename std::conditional<std::is_void<T>::value,
343432
std::nullopt_t, T>::type &&value) {
344433
assert(state_ && "Invalid promise state");
434+
assert(!(status_ & PromiseStatus::ValueSet) && "set_value() can only be called once");
345435
state_->storage.value_ = std::move(value);
436+
status_ |= PromiseStatus::ValueSet;
346437
state_->status_.store(FutureStatus::Ready, std::memory_order_release);
347438
}
348439

@@ -357,21 +448,25 @@ template <typename T> class promise {
357448
template <typename U = T>
358449
typename std::enable_if<std::is_void<U>::value, void>::type set_value() {
359450
assert(state_ && "Invalid promise state");
451+
assert(!(status_ & PromiseStatus::ValueSet) && "set_value() can only be called once");
452+
status_ |= PromiseStatus::ValueSet;
360453
state_->status_.store(FutureStatus::Ready, std::memory_order_release);
361454
}
362455

363456
/// Swap with another promise
364457
void swap(promise &other) noexcept {
365458
using std::swap;
366459
swap(state_, other.state_);
367-
swap(future_created_, other.future_created_);
460+
swap(status_, other.status_);
368461
}
369462

370463
private:
371464
typename future<T>::state *state_;
372-
bool future_created_;
465+
PromiseStatus status_;
373466
};
374467

468+
/// @}
469+
375470
} // End namespace orc
376471
} // End namespace llvm
377472

llvm/lib/ExecutionEngine/Orc/Core.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2735,7 +2735,8 @@ void ExecutionSession::OL_completeLookup(
27352735
auto MR = createMaterializationResponsibility(
27362736
*UMI->RT, std::move(UMI->MU->SymbolFlags),
27372737
std::move(UMI->MU->InitSymbol));
2738-
LLVM_DEBUG(dbgs() << " Dispatching \"" << UMI->MU->getName() << "\"\n");
2738+
LLVM_DEBUG(dbgs()
2739+
<< " Dispatching \"" << UMI->MU->getName() << "\"\n");
27392740
dispatchTask(std::make_unique<MaterializationTask>(std::move(UMI->MU),
27402741
std::move(MR)));
27412742
}

0 commit comments

Comments
 (0)