Uni20
Asynchronous tensor and runtime library.
Loading...
Searching...
No Matches
epoch_context.hpp
Go to the documentation of this file.
1
5
6#pragma once
7
8#include "async_errors.hpp"
9#include "async_node.hpp"
11#include "shared_storage.hpp"
12#include "task_registry.hpp"
13#include <atomic>
14#include <coroutine>
15#include <cstdio>
16#include <cstdlib>
17#include <exception>
18#include <fmt/format.h>
19#include <functional>
20#include <memory>
21#include <mutex>
22#include <vector>
23
24namespace uni20::async
25{
26
27template <typename T> class Async;
28template <typename T> class EpochContextReader;
29template <typename T> class EpochContextWriter;
30
81
82// =============================================================================================
83// EpochContext Invariants
84// =============================================================================================
85//
86// Exactly one of the following must hold at all times:
87//
88// * phase_ == Pending
89//
90// * phase_ == Started
91// && total_writers_ == 0
92//
93// * phase_ == Writing
94// && num_writers_ > 0
95//
96// * phase_ == Reading
97// && num_writers_ == 0
98// && (num_readers_ > 0 || next_epoch_ == nullptr)
99//
100// * phase_ == Finished
101//
102// Normally, num_writers_ <= 1. (Not sure if there is a use for > 1, as a transient?)
103//
104// ---------------------------------------------------------------------------------------------
105//
106// Destruction invariants:
107//
108// * Destruction while (num_writers_ > 0 || num_readers_ > 0)
109// is erroneous (assertion failure).
110//
111// ---------------------------------------------------------------------------------------------
112//
113// Phase transition rules:
114//
115// * Pending → Started
116// Triggered by start_epoch()
117//
118// * Started → Writing
119// Triggered by total_writers_ > 0.
120//
121// * Writing → Reading
122// Triggered when num_writers_ drops to to 0.
123// (i.e. writer has finished).
124// \pre num_writers_ = 0
125//
126// * Reading → Finished
127// Triggered when num_readers_ drops to 0 && next_epoch_ != nullptr
128// or next_epoch_ is set and num_readers == 0
129// Invokes next_epoch_->start() and passes on cancelled_ and eptr_
130// \pre num_readers_ == 0 && next_epoch_ != nullptr
131//
132// * Writing → Finished
133// Triggered when num_writers_ drops to to 0 && num_readers_ == 0 && next_eopch_ != nullptr
134// (i.e. writer has finished, and there are no readers).
135// \pre num_writers_ = 0 && num_readers_ == 0 && next_epoch_ != nullptr
136//
137// Note that immediate transitions are possible.
138//
139// ---------------------------------------------------------------------------------------------
140//
141// Registration rules:
142//
143// * Writers can only be added when phase_ == Pending || phase_ == Started.
144// * Readers can only be added when phase_ != Finished.
145//
146// =============================================================================================
147//
148// Cancellation:
149// An Epoch chain can be cancelled at any time. This indicates that the final result of this
150// calculation is not going to be used and Reader tasks will either need to handle the cancel()
151// or they will get an exception.
152//
153// Initialization status is handled separately. The EpochContext simply handles writers and readers.
154
155// WriteBuffer participates in epoch scheduling only; value construction state is tracked by shared_storage<T>.
156// If a writer is released without emplacing/writing a value, the storage can remain unconstructed for this epoch.
157//
158// If a ReadBuffer's epoch has no value, ordinary reads throw buffer_read_uninitialized while
159// .or_cancel() throws task_cancelled. Exceptions stored by failed writers always propagate unchanged.
160//
161// If there is a next_epoch_, then the cancellation and exception status is propogated forwards.
162//
163// co_await on a WriteBuffer on a cancelled or exception EpochContext passes the error into the coroutine.
164//
165
166class EpochQueue;
167class ReverseEpochQueue;
168
170 public:
171 enum class Phase
172 {
173 Pending,
174 Started,
175 Writing,
176 Reading,
177 Finished
178 };
179
181 {
183 };
184
186 {
189 EpochContext const* next_epoch{nullptr};
193 bool writer_active{false};
194 std::vector<std::coroutine_handle<>> reader_tasks{};
195 std::vector<std::coroutine_handle<>> writer_tasks{};
196#if UNI20_DEBUG_DAG
197 NodeInfo const* node{nullptr};
198#endif
199 };
200
202
205 explicit EpochContext(std::shared_ptr<EpochContext> next) : counter_(next->counter_ - 1), next_epoch_(next)
206 {
207#if UNI20_DEBUG_DAG
208 node_ = next ? next->node() : nullptr;
209#endif
211 }
212
214 static std::shared_ptr<EpochContext> make_previous(std::shared_ptr<EpochContext> next)
215 {
216 return std::make_shared<EpochContext>(std::move(next));
217 }
218
220 {
221 {
222 std::lock_guard lock(mtx_);
223 CHECK_EQUAL(num_writers_, 0);
224 CHECK_EQUAL(num_readers_, 0);
225 }
227 }
228
230 void set_next_epoch(std::shared_ptr<EpochContext> next)
231 {
232 std::unique_lock lock(mtx_);
233 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::set_next_epoch", this, next.get(), counter_);
234 DEBUG_CHECK(!next_epoch_); // make sure that we haven't already set the next epoch pointer
235 DEBUG_CHECK(phase_ <= Phase::Reading);
236
237 next->counter_ = counter_ + 1; // increment the epoch counter
238#if UNI20_DEBUG_DAG
239 next->node_ = node_;
240#endif
241 next_epoch_ = std::move(next);
242 if (phase_ == Phase::Reading && num_readers_ == 0) this->advance_finished_locked(std::move(lock));
243 }
244
245 // start executing the epoch, if it has not already started
246 void start()
247 {
248 std::unique_lock lock(mtx_);
249 // DEBUG_CHECK(phase_ == Phase::Pending);
250 if (phase_ == Phase::Pending) this->advance_start_locked(std::move(lock));
251 }
252
253 // start executing the epoch, assuming that we've already written
254 // precondition: no existing writers
256 {
257 std::unique_lock lock(mtx_);
258 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::start_reading", this, counter_);
259 DEBUG_CHECK(phase_ == Phase::Pending);
260 DEBUG_CHECK(total_writers_ == 0);
261 ++total_writers_;
262 this->advance_start_locked(std::move(lock));
263 }
264
265 // start executing the epoch, propogating the exception and cancellation state from a previous epoch
266 void start(std::exception_ptr eptr)
267 {
268 std::unique_lock lock(mtx_);
269 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::start", this, counter_);
270 DEBUG_CHECK(phase_ == Phase::Pending, this);
271 eptr_ = eptr;
272 this->advance_start_locked(std::move(lock));
273 }
274
277 {
278 std::lock_guard lock(mtx_);
279 DebugSnapshot snapshot;
280 snapshot.generation = counter_;
281 snapshot.phase = phase_;
282 snapshot.next_epoch = next_epoch_.get();
283 snapshot.total_writers = total_writers_;
284 snapshot.num_writers = num_writers_;
285 snapshot.num_readers = num_readers_;
286 snapshot.writer_active = writer_active_;
287#if UNI20_DEBUG_DAG
288 snapshot.node = node_;
289#endif
290
291 snapshot.reader_tasks.reserve(reader_tasks_.size());
292 for (auto const& task_state : reader_tasks_)
293 {
294 if (auto h = task_state.task.coroutine_handle()) snapshot.reader_tasks.push_back(h);
295 }
296
297 snapshot.writer_tasks.reserve(writer_tasks_.size());
298 for (auto const& task_state : writer_tasks_)
299 {
300 if (auto h = task_state.task.coroutine_handle()) snapshot.writer_tasks.push_back(h);
301 }
302
303 return snapshot;
304 }
305
309 bool try_debug_snapshot(DebugSnapshot& snapshot) const
310 {
311 if (!mtx_.try_lock()) return false;
312 std::lock_guard lock(mtx_, std::adopt_lock);
313 snapshot = DebugSnapshot{};
314 snapshot.generation = counter_;
315 snapshot.phase = phase_;
316 snapshot.next_epoch = next_epoch_.get();
317 snapshot.total_writers = total_writers_;
318 snapshot.num_writers = num_writers_;
319 snapshot.num_readers = num_readers_;
320 snapshot.writer_active = writer_active_;
321#if UNI20_DEBUG_DAG
322 snapshot.node = node_;
323#endif
324
325 snapshot.reader_tasks.reserve(reader_tasks_.size());
326 for (auto const& task_state : reader_tasks_)
327 {
328 if (auto h = task_state.task.coroutine_handle()) snapshot.reader_tasks.push_back(h);
329 }
330
331 snapshot.writer_tasks.reserve(writer_tasks_.size());
332 for (auto const& task_state : writer_tasks_)
333 {
334 if (auto h = task_state.task.coroutine_handle()) snapshot.writer_tasks.push_back(h);
335 }
336
337 return true;
338 }
339
341 std::vector<std::coroutine_handle<>> reader_task_handles() const
342 {
343 auto snapshot = this->debug_snapshot();
344 return std::move(snapshot.reader_tasks);
345 }
346
348 std::vector<std::coroutine_handle<>> writer_task_handles() const
349 {
350 auto snapshot = this->debug_snapshot();
351 return std::move(snapshot.writer_tasks);
352 }
353
354#if UNI20_DEBUG_DAG
357 NodeInfo const* node() const
358 {
359 std::lock_guard lock(mtx_);
360 return node_;
361 }
362
365 void set_node(NodeInfo const* node)
366 {
367 std::lock_guard lock(mtx_);
368 node_ = node;
369 }
370#endif
371
372 private:
373 // Writer interface
374 // internal private used only by EpochContextWriter<T>
375 template <typename T> friend class EpochContextWriter;
376 friend void propagate_unhandled_writer_exception(EpochContext* epoch, std::exception_ptr eptr) noexcept;
377
380 void writer_acquire() noexcept
381 {
382 std::unique_lock lock(mtx_);
383 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::writer_acquire", this, counter_, phase_);
384 // TODO: if we have reenterant writers, then we might be able to acquire writers in Writing phase
385 DEBUG_CHECK(phase_ <= Phase::Started);
386
387 ++num_writers_;
388 ++total_writers_;
389 if (phase_ == Phase::Started) this->advance_writing_locked(std::move(lock));
390 }
391
392 bool has_writer() const noexcept
393 {
394 std::lock_guard lock(mtx_);
395 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::has_writer", this, counter_, phase_, total_writers_);
396 return total_writers_ > 0 || phase_ >= Phase::Writing;
397 }
398
400 void writer_clear_errors() noexcept
401 {
402 std::lock_guard lock(mtx_);
403 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::writer_clear_errors", this, counter_);
404 DEBUG_CHECK(phase_ <= Phase::Writing);
405 eptr_ = nullptr;
406 // cancelled_ = false;
407 // inherit_error_state_ = false;
408 }
409
413 void writer_bind(BasicTask&& task) noexcept
414 {
415 std::unique_lock lock(mtx_);
416 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::writer_bind", this, counter_, task.coroutine_handle(), phase_,
417 num_writers_, writer_active_);
418 DEBUG_CHECK(phase_ <= Phase::Writing);
419 DEBUG_CHECK(phase_ != Phase::Started); // if this Epoch was started, it should have transitioned to Writing by now
420
421 // If the Epoch is running, then execute the task immediately
422 if (phase_ == Phase::Writing && !writer_active_)
423 {
424 this->execute_writer_locked(std::move(lock), TaskState{std::move(task)});
425 return;
426 }
427
428 // Otherwise, store the task for later
429 writer_tasks_.emplace_back(std::move(task));
430 }
431
434 bool writer_try_make_active() noexcept
435 {
436 std::unique_lock lock(mtx_);
437 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::writer_try_make_active", this, counter_, phase_, writer_active_);
438 DEBUG_CHECK(phase_ <= Phase::Writing);
439 if (phase_ < Phase::Writing || writer_active_) return false;
440 writer_active_ = true;
441 return true;
442 }
443
444 // Notify about availability, without reserving the gate. The waiting writer
445 // still acquires through writer_try_make_active() and may need to wait again.
446 void writer_notify_when_ready(std::function<void()> notify)
447 {
448 std::unique_lock lock(mtx_);
449 DEBUG_CHECK(phase_ <= Phase::Writing);
450 if (phase_ == Phase::Writing && !writer_active_)
451 {
452 lock.unlock();
453 notify();
454 return;
455 }
456 writer_ready_waiters_.push_back(std::move(notify));
457 }
458
459 void writer_set_exception(std::exception_ptr e) noexcept
460 {
461 std::lock_guard lock(mtx_);
462 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::writer_set_exception", this, counter_);
463 DEBUG_CHECK(phase_ <= Phase::Reading);
464 if (!eptr_) eptr_ = e;
465 }
466
467 // void writer_set_cancel() noexcept
468 // {
469 // std::lock_guard lock(mtx_);
470 // DEBUG_TRACE_MODULE(ASYNC, "EpochContext::writer_set_cancel", this, counter_);
471 // DEBUG_CHECK(phase_ == Phase::Writing);
472 // cancelled_ = true;
473 // }
474
478 bool writer_cancel() noexcept
479 {
480 std::unique_lock lock(mtx_);
481 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::writer_release", this, counter_, phase_);
482 DEBUG_CHECK(phase_ <= Phase::Writing);
483 DEBUG_CHECK(num_writers_ >= 1);
484
485 --num_writers_;
486
487 if (num_writers_ == 0 && phase_ == Phase::Writing)
488 {
489 DEBUG_CHECK(!writer_active_);
490 this->advance_reading_locked(std::move(lock));
491 return true;
492 }
493 return false;
494 }
495
500 bool writer_release_active() noexcept
501 {
502 std::unique_lock lock(mtx_);
503 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::writer_release", this, counter_, phase_);
504 DEBUG_CHECK(phase_ == Phase::Writing);
505 DEBUG_CHECK(num_writers_ >= 1);
506 DEBUG_CHECK(writer_active_);
507
508 writer_active_ = false;
509 --num_writers_;
510
511 if (num_writers_ == 0 && phase_ == Phase::Writing)
512 {
513 this->advance_reading_locked(std::move(lock));
514 return true;
515 }
516
517 if (!writer_tasks_.empty())
518 {
519 auto NextTask = std::move(writer_tasks_.back());
520 writer_tasks_.pop_back();
521 this->execute_writer_locked(std::move(lock), std::move(NextTask));
522 }
523 else
524 {
525 this->notify_writers_locked(std::move(lock));
526 }
527
528 return false;
529 }
530
531 private:
532 // reader interface
533 // internal private used only by EpochContextReader<T>
534 template <typename T> friend class EpochContextReader;
535
540 void reader_acquire() noexcept
541 {
542 std::lock_guard lock(mtx_);
543 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::reader_acquire", this, counter_);
544 DEBUG_CHECK(phase_ <= Phase::Reading);
545
546 ++num_readers_;
547 }
548
550 bool reader_ready() const noexcept
551 {
552 std::lock_guard lock(mtx_);
553 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::reader_ready", this, counter_);
554 DEBUG_CHECK(phase_ <= Phase::Reading);
555
556 return phase_ == Phase::Reading;
557 }
558
561 void reader_notify_when_ready(std::function<void()> notify)
562 {
563 std::unique_lock lock(mtx_);
564 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::reader_notify_when_ready", this, counter_, phase_);
565 DEBUG_CHECK(phase_ <= Phase::Reading);
566
567 if (phase_ == Phase::Reading)
568 {
569 lock.unlock();
570 notify();
571 return;
572 }
573 reader_ready_waiters_.push_back(std::move(notify));
574 }
575
576 void reader_bind(BasicTask&& h)
577 {
578 std::unique_lock lock(mtx_);
579 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::reader_bind", this, counter_, phase_);
580 DEBUG_CHECK(phase_ <= Phase::Reading);
581
582 if (phase_ == Phase::Reading)
583 {
584 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::reader_bind is scheduling the task immediately", this, counter_);
585 std::exception_ptr my_eptr = eptr_;
586 lock.unlock();
587 if (my_eptr) h.exception_on_resume(my_eptr);
588 BasicTask::reschedule(std::move(h));
589 }
590 else
591 {
592 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::reader_bind is adding the reader task", h.coroutine_handle());
593 reader_tasks_.emplace_back(std::move(h));
594 }
595 }
596
598 std::exception_ptr reader_exception() const noexcept
599 {
600 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::reader_exception", this, counter_);
602
603 return eptr_;
604 }
605
606 // bool reader_cancelled() const noexcept
607 // {
608 // DEBUG_TRACE_MODULE(ASYNC, "EpochContext::reader_bind", this, counter_);
609 // DEBUG_PRECONDITION(phase_ == Phase::Reading);
610 //
611 // return cancelled_;
612 // }
613 //
614 bool reader_release() noexcept
615 {
616 std::lock_guard lock(mtx_);
617 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::reader_release", this, counter_);
618 DEBUG_CHECK(phase_ <= Phase::Reading);
619 DEBUG_CHECK(num_readers_ >= 1);
620
621 --num_readers_;
622
623 if (phase_ == Phase::Reading && num_readers_ == 0 && next_epoch_ != nullptr)
624 {
625 phase_ = Phase::Finished;
626 next_epoch_->start(eptr_);
627 }
628
629 return num_readers_ == 0;
630 }
631
632 // Helper functions
633 void advance_start_locked(std::unique_lock<std::mutex> lock)
634 {
635 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::advance_start_locked", this, counter_);
637 phase_ = Phase::Started;
638
639 if (total_writers_ > 0) this->advance_writing_locked(std::move(lock));
640 }
641
642 void advance_writing_locked(std::unique_lock<std::mutex> lock) noexcept
643 {
644 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::advance_writing_locked", this, counter_);
646 phase_ = Phase::Writing;
647
648 if (!writer_tasks_.empty())
649 {
650 auto NextTask = std::move(writer_tasks_.back());
651 writer_tasks_.pop_back();
652 this->execute_writer_locked(std::move(lock), std::move(NextTask));
653 return;
654 }
655
656 if (num_writers_ == 0)
657 this->advance_reading_locked(std::move(lock));
658 else
659 this->notify_writers_locked(std::move(lock));
660 }
661
662 void advance_reading_locked(std::unique_lock<std::mutex> lock) noexcept
663 {
664 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::advance_reading_locked", this, counter_);
666 phase_ = Phase::Reading;
667
668 if (num_readers_ > 0)
669 {
670 this->execute_readers_locked(std::move(lock));
671 return;
672 }
673
674 if (num_readers_ == 0 && next_epoch_ != nullptr) this->advance_finished_locked(std::move(lock));
675 }
676
677 void advance_finished_locked(std::unique_lock<std::mutex> lock) noexcept
678 {
679 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::advance_finished_locked", this, counter_);
680 DEBUG_PRECONDITION(phase_ == Phase::Reading && next_epoch_);
681 phase_ = Phase::Finished;
682
683 if (next_epoch_)
684 {
685 next_epoch_->start(eptr_);
686 }
687 }
688
689 void execute_writer_locked(std::unique_lock<std::mutex> lock, TaskState task)
690 {
691 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::execute_writer_locked", this, counter_);
692 DEBUG_CHECK(phase_ == Phase::Writing);
693 DEBUG_CHECK(!writer_active_);
694
695 if (eptr_) task.task.exception_on_resume(eptr_);
696 writer_active_ = true;
697 lock.unlock();
698 BasicTask::reschedule(std::move(task.task));
699 }
700
701 void notify_writers_locked(std::unique_lock<std::mutex> lock) noexcept
702 {
703 DEBUG_CHECK(phase_ == Phase::Writing && !writer_active_);
704 // As with reader callbacks, notification can release the last epoch owner.
705 // Move callbacks out and do not access this after unlocking.
706 std::vector<std::function<void()>> waiters;
707 waiters.swap(writer_ready_waiters_);
708 lock.unlock();
709 for (auto& notify : waiters)
710 notify();
711 }
712
713 void execute_readers_locked(std::unique_lock<std::mutex> lock)
714 {
715 DEBUG_TRACE_MODULE(ASYNC, "EpochContext::execute_readers_locked", this, counter_);
716
717 // When we reschedule (or cancel) a task, we need to assume that the coroutine frame may get
718 // destroyed at any time, which might result in the last reference to this EpochContext itself
719 // disappearing. This means that we cannot reference *this at all once we start rescheduling
720 // tasks. Hence copy all of the data structures we need into local storage and release the lock.
721 std::exception_ptr my_eptr = eptr_;
722 std::vector<TaskState> my_reader_tasks;
723 std::swap(reader_tasks_, my_reader_tasks);
724 std::vector<std::function<void()>> my_ready_waiters;
725 std::swap(reader_ready_waiters_, my_ready_waiters);
726 lock.unlock();
727
728 for (auto& notify : my_ready_waiters)
729 {
730 notify();
731 }
732
733 for (auto&& task : my_reader_tasks)
734 {
735 if (my_eptr) task.task.exception_on_resume(my_eptr);
736 BasicTask::reschedule(std::move(task.task));
737 }
738 }
739
740 friend class EpochQueue;
741 friend class ReverseEpochQueue;
742
743 // All of the data is protected by the mutex
744 mutable std::mutex mtx_;
745
746 // Exception pointer - if this is set, then any attempt to get a buffer throws the exception.
747 // This is also propogated to future epochs
748 std::exception_ptr eptr_{nullptr};
749
750 // Epoch counter. This is mainly for debugging
751 int counter_{0};
752
753 // Pointer to the following epoch
754 std::shared_ptr<EpochContext> next_epoch_;
755
756 Phase phase_{Phase::Pending};
757
758 // Writer interface
759 int total_writers_{0};
760 int num_writers_{0};
761 std::vector<TaskState> writer_tasks_;
762 std::vector<std::function<void()>> writer_ready_waiters_;
763 bool writer_active_{false};
764
765 // Reader interface
766 int num_readers_{0};
767 std::vector<TaskState> reader_tasks_;
768 std::vector<std::function<void()>> reader_ready_waiters_;
769
770#if UNI20_DEBUG_DAG
771 NodeInfo const* node_{nullptr};
772#endif
773
774 // NOTE: we should be able to use atomic operations for most data.
775 // Data that is only written once in some phase, and read only in a later phase,
776 // do not need any protection at all because it is sequenced by the phase change.
777 // std::atomic<Phase> phase_{Phase::Pending};
778 // std::atomic<int> num_writers_{0};
779 // std::atomic<int> num_readers_{0};
780 // std::shared_ptr<EpochContext> next_epoch_; // shared_ptr for lifetime management
781 // std::atomic<EpochContext*> next_epoch_ptr_; // copy of next_epoch_ in an atomic pointer
782 //
783 // bool inherit_error_state{true}; // written only before phase_ = Reading
784 // bool cancelled_{false}; // written only before phase_ = Reading
785 // std::exception_ptr eptr_{nullptr}; // written only before phase_ = Reading
786 // BasicTask writer_tasks_; // written only before phase_ = Writing
787 // std::vector<BasicTask> reader_tasks_; // protected by mutex
788 // std::mutex reader_tasks_mtx_; // protects reader_tasks_
789 //
790 // starting next_epoch_ is gated on phase_ transitioning to Finished.
791 // This is a competition between set_next_epoch() and release_writer().
792 //
793 // void set_next_epoch(std::shared_ptr<EpochContext> next) {
794 // DEBUG_TRACE_MODULE(ASYNC, "EpochContext::set_next_epoch", this, next.get(), counter_);
795 //
796 // DEBUG_CHECK(!next_epoch_); // not set before
797 // DEBUG_CHECK(phase_.load(std::memory_order_relaxed) <= Phase::Reading);
798 //
799 // // Establish ownership first
800 // next->counter_ = counter_ + 1;
801 // next_epoch_ = std::move(next);
802 //
803 // // Publish raw pointer with release semantics
804 // next_epoch_ptr_.store(next_epoch_.get(), std::memory_order_release);
805 //
806 // // If readers are already done, start the next epoch now
807 // if (num_readers_ == 0) {
808 // Phase expected = Phase::Reading;
809 // if (phase_.compare_exchange_strong(expected, Phase::Finished, std::memory_order_acq_rel)) {
810 // auto* next_ptr = next_epoch_ptr_.load(std::memory_order_acquire);
811 // if (next_ptr)
812 // next_ptr->start(eptr_, cancelled_);
813 // }
814 // }
815 // }
816 //
817 // void reader_release() noexcept
818 // {
819 // int readers = --num_readers_;
820 //
821 // if (readers == 0)
822 // {
823 // auto* next_ptr = next_epoch_ptr_.load(std::memory_order_acquire);
824 // if (next_ptr && phase_.load(std::memory_order_relaxed) == Phase::Reading)
825 // {
826 // Phase expected = Phase::Reading;
827 // if (phase_.compare_exchange_strong(expected, Phase::Finished, std::memory_order_acq_rel))
828 // {
829 // next_ptr->start(eptr_, cancelled_);
830 // }
831 // }
832 // }
833 // }
834};
835
836inline void propagate_unhandled_writer_exception(EpochContext* epoch, std::exception_ptr eptr) noexcept
837{
838 if (!epoch || !eptr) return;
839 epoch->writer_set_exception(std::move(eptr));
840}
841
843{
845 switch (p)
846 {
847 case Phase::Pending:
848 return "Pending";
849 case Phase::Started:
850 return "Started";
851 case Phase::Writing:
852 return "Writing";
853 case Phase::Reading:
854 return "Reading";
855 case Phase::Finished:
856 return "Finished";
857 }
858 return "Unknown";
859}
860
873
874template <typename T> class EpochContextReader {
875 public:
878
880 EpochContextReader(EpochContextReader const& other) : storage_(other.storage_), epoch_(other.epoch_)
881 {
882 epoch_->reader_acquire();
883 }
884
889 {
890 if (this != &other)
891 {
892 if (other.epoch_) other.epoch_->reader_acquire();
893 this->release();
894 storage_ = other.storage_;
895 epoch_ = other.epoch_;
896 }
897 return *this;
898 }
899
903 EpochContextReader(shared_storage<T> storage, std::shared_ptr<EpochContext> epoch) noexcept
904 : storage_(std::move(storage)), epoch_(std::move(epoch))
905 {
906 epoch_->reader_acquire();
907 }
908
912 : storage_(std::move(other.storage_)), epoch_(std::move(other.epoch_))
913 {}
914
919 {
920 if (this != &other)
921 {
922 this->release();
923 storage_ = std::move(other.storage_);
924 epoch_ = std::move(other.epoch_);
925 }
926 return *this;
927 }
928
931
932#if UNI20_DEBUG_DAG
934 NodeInfo const* node() const { return epoch_ ? epoch_->node() : nullptr; }
935#endif
936
940 {
941 TRACE_MODULE(ASYNC, "EpochContextReader::suspend", this, t.coroutine_handle(), epoch_.get(), epoch_->counter_);
942 epoch_->reader_bind(std::move(t));
943 }
944
947 [[nodiscard]] bool ready() const noexcept { return epoch_->reader_ready(); }
948
951 void notify_when_ready(std::function<void()> notify) const { epoch_->reader_notify_when_ready(std::move(notify)); }
952
957 [[nodiscard]] T const& data() const
958 {
959 DEBUG_PRECONDITION(epoch_->reader_ready());
960 DEBUG_TRACE_MODULE(ASYNC, "EpochContextReader::data", epoch_.get(), epoch_->counter_);
961 if (auto e = epoch_->reader_exception(); e) std::rethrow_exception(e);
962 auto* ptr = storage_.get();
963 if (!ptr)
964 {
965#if UNI20_DEBUG_ASYNC_TASKS
966 constexpr char const* reason = "EpochContextReader::data access on uninitialized storage";
967 auto const mode = TaskRegistry::dump_mode();
969 {
970 auto const epoch_counter = epoch_ ? epoch_->counter_ : -1;
971 TRACE_STACK(reason, this, epoch_.get(), epoch_counter);
974 else
975 TaskRegistry::dump_epoch_context(epoch_.get(), reason);
976 }
977#endif
979 }
980 return *ptr;
981 }
982
983 // \brief Optionally retrieves the value stored in this buffer, if available.
988 [[nodiscard]] T const* data_maybe() const
989 {
990 DEBUG_PRECONDITION(epoch_->reader_ready());
991 if (auto e = epoch_->reader_exception(); e) std::rethrow_exception(e);
992 return storage_.get();
993 }
994
1001 [[nodiscard]] std::optional<T> data_option() const
1002 {
1003 DEBUG_PRECONDITION(epoch_->reader_ready());
1004 if (auto e = epoch_->reader_exception(); e) std::rethrow_exception(e);
1005 auto* ptr = storage_.get();
1006 if (!ptr) return std::nullopt;
1007 return *ptr;
1008 }
1009
1011 void release() noexcept
1012 {
1013 if (epoch_)
1014 {
1015 epoch_->reader_release();
1016 epoch_.reset();
1017 }
1018 }
1019
1021 void wait() const;
1022
1025 void wait(IAsyncScheduler& sched) const;
1026
1028 [[nodiscard]] T const& get_wait() const;
1029
1032 [[nodiscard]] T const& get_wait(IAsyncScheduler& sched) const;
1033
1034 [[nodiscard]] std::shared_ptr<EpochContext> epoch_context_shared() const noexcept { return epoch_; }
1035
1036 private:
1037 shared_storage<T> storage_;
1038 std::shared_ptr<EpochContext> epoch_{};
1039};
1040
1048template <typename T> class EpochContextWriter {
1049 public:
1051
1055 EpochContextWriter(shared_storage<T> storage, std::shared_ptr<EpochContext> epoch) noexcept
1056 : storage_(std::move(storage)), epoch_(std::move(epoch))
1057 {
1058 TRACE_MODULE(ASYNC, "EpochContextWriter Constructor", this, epoch_.get());
1059 epoch_->writer_acquire();
1060 }
1061
1063 // EpochContextWriter(EpochContextWriter const& other) : storage_(other.storage_), epoch_(other.epoch_)
1064 // {
1065 // epoch_->writer_acquire_duplicate(static_cast<void const*>(this));
1066 // }
1067
1068 explicit operator bool() const noexcept { return epoch_ != nullptr; }
1069
1071
1075 : storage_(std::move(other.storage_)), epoch_(std::move(other.epoch_)), acquired_(std::move(other.acquired_))
1076 {
1077 other.epoch_.reset();
1078 }
1079
1084 {
1085 if (this != &other)
1086 {
1087 this->release();
1088 storage_ = std::move(other.storage_);
1089 epoch_ = std::move(other.epoch_);
1090 acquired_ = other.acquired_;
1091 other.epoch_.reset();
1092 }
1093 return *this;
1094 }
1095
1098
1099#if UNI20_DEBUG_DAG
1101 NodeInfo const* node() const { return epoch_ ? epoch_->node() : nullptr; }
1102#endif
1103
1106 bool ready() const noexcept
1107 {
1108 if (acquired_) return true;
1109 acquired_ = epoch_->writer_try_make_active();
1110 return acquired_;
1111 }
1112
1116 {
1117 DEBUG_CHECK(!acquired_);
1118 epoch_->writer_bind(std::move(t));
1119 }
1120
1121 void resume() const noexcept { acquired_ = true; }
1122
1126 T& data() const
1127 {
1128 auto* ptr = storage_.get();
1129 if (!ptr)
1130 {
1131#if UNI20_DEBUG_ASYNC_TASKS
1132 constexpr char const* reason = "EpochContextWriter::data access on uninitialized storage";
1133 auto const mode = TaskRegistry::dump_mode();
1134 if (mode != TaskRegistry::DumpMode::None)
1135 {
1136 auto const epoch_counter = epoch_ ? epoch_->counter_ : -1;
1137 TRACE_STACK(reason, this, epoch_.get(), epoch_counter);
1138 if (mode == TaskRegistry::DumpMode::Full)
1140 else
1141 TaskRegistry::dump_epoch_context(epoch_.get(), reason);
1142 }
1143#endif
1145 }
1146 return *ptr;
1147 }
1148
1149 template <typename... Args>
1150 requires std::constructible_from<T, Args...>
1151 T& emplace(Args&&... args)
1152 {
1153 try
1154 {
1155 return storage_.emplace(std::forward<Args>(args)...);
1156 }
1157 catch (...)
1158 {
1159#if UNI20_DEBUG_ASYNC_TASKS
1160 constexpr char const* reason = "EpochContextWriter::emplace failed while constructing storage";
1161 auto const mode = TaskRegistry::dump_mode();
1162 if (mode != TaskRegistry::DumpMode::None)
1163 {
1164 auto const epoch_counter = epoch_ ? epoch_->counter_ : -1;
1165 TRACE_STACK(reason, this, epoch_.get(), epoch_counter);
1166 if (mode == TaskRegistry::DumpMode::Full)
1168 else
1169 TaskRegistry::dump_epoch_context(epoch_.get(), reason);
1170 }
1171#endif
1172 throw;
1173 }
1174 }
1175
1176 shared_storage<T>& storage() { return storage_; }
1177
1178 void set_exception(std::exception_ptr e) const noexcept { epoch_->writer_set_exception(e); }
1179
1180 std::shared_ptr<EpochContext> epoch_context_shared() const noexcept { return epoch_; }
1181
1184 bool value_is_initialized() const noexcept { return storage_.constructed(); }
1185
1188 void release() noexcept
1189 {
1190 if (epoch_)
1191 {
1192 if (acquired_)
1193 epoch_->writer_release_active();
1194 else
1195 epoch_->writer_cancel(); // releasing without ever
1196 epoch_.reset();
1197 storage_.reset();
1198 }
1199 }
1200
1202 T&& move_from_wait();
1203
1204 private:
1205 mutable shared_storage<T> storage_;
1206 std::shared_ptr<EpochContext> epoch_;
1207 mutable bool acquired_{false};
1208};
1209
1210} // namespace uni20::async
Exception hierarchy for async/dataflow operations.
Debug metadata nodes used for async DAG visualization.
Defines shared and concrete task coroutine promises.
static void dump()
Prints global task-registry diagnostics.
static void register_epoch_context(async::EpochContext const *epoch_context)
Registers an epoch context for debug tracking.
static void dump_epoch_context(async::EpochContext const *epoch_context, char const *reason=nullptr)
Prints diagnostics for a specific epoch context.
static DumpMode dump_mode() noexcept
Reports the current debug dump mode.
static void destroy_epoch_context(async::EpochContext const *epoch_context)
Removes an epoch context from debug tracking.
Move-only promise-neutral ownership claim for a Uni20 coroutine.
Definition async_task.hpp:103
static void reschedule(BasicTask task)
Reschedule a task when it becomes the sole ownership claimant.
Definition async_task_impl.hpp:11
RAII handle representing the writer of a value in an EpochContext.
Definition epoch_context.hpp:874
std::shared_ptr< EpochContext > epoch_context_shared() const noexcept
Definition epoch_context.hpp:1034
~EpochContextReader()
Destructor. Signals epoch reader completion if still active.
Definition epoch_context.hpp:930
std::optional< T > data_option() const
Optionally retrieves the value stored in this buffer, if available.
Definition epoch_context.hpp:1001
EpochContextReader & operator=(EpochContextReader &&other) noexcept
Move assignment.
Definition epoch_context.hpp:918
EpochContextReader(EpochContextReader const &other)
Copy constructor retaining the reader reference count.
Definition epoch_context.hpp:880
bool ready() const noexcept
Check whether the reader is ready to resume.
Definition epoch_context.hpp:947
EpochContextReader(shared_storage< T > storage, std::shared_ptr< EpochContext > epoch) noexcept
Construct a new reader handle for a given parent and epoch.
Definition epoch_context.hpp:903
void release() noexcept
Release the reader.
Definition epoch_context.hpp:1011
void suspend(BasicTask &&t)
Suspend a coroutine task as a reader of this epoch.
Definition epoch_context.hpp:939
void wait() const
Wait for the epoch to become available on the global scheduler.
Definition debug_scheduler.hpp:274
T const * data_maybe() const
Definition epoch_context.hpp:988
T const & data() const
Access the stored value inside the parent Async<T>.
Definition epoch_context.hpp:957
EpochContextReader()=delete
Default-constructed inactive reader (no effect).
EpochContextReader & operator=(EpochContextReader const &other)
Copy assignment retaining the reader reference count.
Definition epoch_context.hpp:888
void notify_when_ready(std::function< void()> notify) const
Register a callback for this reader becoming ready.
Definition epoch_context.hpp:951
T const & get_wait() const
Wait for the epoch to become available on the global scheduler and return a reference to the value.
Definition debug_scheduler.hpp:308
EpochContextReader(EpochContextReader &&other) noexcept
Move constructor.
Definition epoch_context.hpp:911
RAII-scoped representation of a writer’s participation in an epoch.
Definition epoch_context.hpp:1048
void suspend(BasicTask &&t)
Suspend the writer task and submit to the epoch context.
Definition epoch_context.hpp:1115
void resume() const noexcept
Definition epoch_context.hpp:1121
EpochContextWriter & operator=(EpochContextWriter &&other) noexcept
Move assignment transferring the writer handle.
Definition epoch_context.hpp:1083
std::shared_ptr< EpochContext > epoch_context_shared() const noexcept
Definition epoch_context.hpp:1180
~EpochContextWriter()
Destructor releasing any held writer epoch.
Definition epoch_context.hpp:1097
bool ready() const noexcept
Check whether the writer may proceed immediately.
Definition epoch_context.hpp:1106
T & emplace(Args &&... args)
Definition epoch_context.hpp:1151
shared_storage< T > & storage()
Definition epoch_context.hpp:1176
EpochContextWriter(shared_storage< T > storage, std::shared_ptr< EpochContext > epoch) noexcept
Construct an active writer.
Definition epoch_context.hpp:1055
T & data() const
Access the stored data while holding the writer gate.
Definition epoch_context.hpp:1126
void release() noexcept
Wait for the epoch to become available, and then return a prvalue-reference to the value.
Definition epoch_context.hpp:1188
void set_exception(std::exception_ptr e) const noexcept
Definition epoch_context.hpp:1178
bool value_is_initialized() const noexcept
Report whether the associated value is currently initialized.
Definition epoch_context.hpp:1184
EpochContextWriter(EpochContextWriter &&other) noexcept
Move constructor transferring the writer handle.
Definition epoch_context.hpp:1074
T && move_from_wait()
Wait for the epoch to become available, and then return a prvalue-reference to the value.
Definition debug_scheduler.hpp:327
EpochContextWriter & operator=(EpochContextWriter const &)=delete
Definition epoch_context.hpp:169
static std::shared_ptr< EpochContext > make_previous(std::shared_ptr< EpochContext > next)
Helper factory to construct the previous epoch in a reverse-mode chain.
Definition epoch_context.hpp:214
DebugSnapshot debug_snapshot() const
Return a point-in-time snapshot of epoch state and queued tasks.
Definition epoch_context.hpp:276
std::vector< std::coroutine_handle<> > writer_task_handles() const
Return coroutine handles currently queued as writers in this epoch context.
Definition epoch_context.hpp:348
std::vector< std::coroutine_handle<> > reader_task_handles() const
Return coroutine handles currently queued as readers in this epoch context.
Definition epoch_context.hpp:341
EpochContext()
Definition epoch_context.hpp:201
void start(std::exception_ptr eptr)
Definition epoch_context.hpp:266
bool try_debug_snapshot(DebugSnapshot &snapshot) const
Try to return a point-in-time snapshot without blocking.
Definition epoch_context.hpp:309
friend void propagate_unhandled_writer_exception(EpochContext *epoch, std::exception_ptr eptr) noexcept
Inject an unhandled writer-side coroutine exception into an epoch.
Definition epoch_context.hpp:836
EpochContext(std::shared_ptr< EpochContext > next)
Construct an EpochContext given an existing next epoch.
Definition epoch_context.hpp:205
void set_next_epoch(std::shared_ptr< EpochContext > next)
Set the next EpochContext in the chain.
Definition epoch_context.hpp:230
void start_reading()
Definition epoch_context.hpp:255
~EpochContext()
Definition epoch_context.hpp:219
void start()
Definition epoch_context.hpp:246
Phase
Definition epoch_context.hpp:172
@ Reading
The epoch is active and waiting for readers to finish.
@ Pending
The epoch is not yet active (i.e. it is in the future)
@ Started
The epoch has started but is waiting for a writer task.
@ Finished
The epoch is not active, and readers have finished.
@ Writing
The epoch is active and waiting for the writers to finish, with no writer currently running.
Coordinates multiple epochs of read/write gates.
Definition epoch_queue.hpp:22
Scheduler interface for initial AsyncTask submission and host-side progress.
Definition scheduler.hpp:89
Records node information for DAG/debugging visualization.
Definition async_node.hpp:171
Reverse-order epoch queue used by reverse-mode gradient accumulation.
Definition epoch_queue.hpp:96
Raised when a reader attempts to access an uninitialized buffer.
Definition async_errors.hpp:68
Raised when a writer requests a mutable reference before construction.
Definition async_errors.hpp:74
A lightweight, thread-safe, reference-counted storage for a single object.
Definition shared_storage.hpp:94
This represents the reverse-mode adjoint for functions of complex variables, assuming a real-valued s...
Definition async/async.hpp:26
constexpr std::string_view format_as(uni20::async::EpochContext::Phase p)
Definition epoch_context.hpp:842
void propagate_unhandled_writer_exception(EpochContext *epoch, std::exception_ptr eptr) noexcept
Inject an unhandled writer-side coroutine exception into an epoch.
Definition epoch_context.hpp:836
Definition data_table.cpp:8
Reference-counted optional in-place storage used by async buffers.
Definition epoch_context.hpp:186
int generation
Definition epoch_context.hpp:187
Phase phase
Definition epoch_context.hpp:188
EpochContext const * next_epoch
Definition epoch_context.hpp:189
int num_writers
Definition epoch_context.hpp:191
std::vector< std::coroutine_handle<> > writer_tasks
Definition epoch_context.hpp:195
int num_readers
Definition epoch_context.hpp:192
int total_writers
Definition epoch_context.hpp:190
std::vector< std::coroutine_handle<> > reader_tasks
Definition epoch_context.hpp:194
bool writer_active
Definition epoch_context.hpp:193
Definition epoch_context.hpp:181
BasicTask task
Definition epoch_context.hpp:182
Selects the task-registry implementation for the current build mode.
#define DEBUG_CHECK(cond,...)
Definition trace.hpp:608
#define TRACE_MODULE(m,...)
Definition trace.hpp:149
#define DEBUG_PRECONDITION(cond,...)
Definition trace.hpp:648
#define DEBUG_TRACE_MODULE(m,...)
Definition trace.hpp:511
#define CHECK_EQUAL(a, b,...)
Definition trace.hpp:309
#define TRACE_STACK(...)
Definition trace.hpp:192