diff --git a/emper/AbstractFiber.cpp b/emper/AbstractFiber.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0db3ee122eb1805c2f1019cde22f6234f178f78c
--- /dev/null
+++ b/emper/AbstractFiber.cpp
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright © 2022 Florian Schmaus
+#include "AbstractFiber.hpp"
+
+auto operator<<(std::ostream& strm, const AbstractFiber& fiber) -> std::ostream& {
+	fiber.printTo(strm, false);
+	return strm;
+}
+
+auto operator<<=(std::ostream& strm, const AbstractFiber& fiber) -> std::ostream& {
+	fiber.printTo(strm, true);
+	return strm;
+}
diff --git a/emper/AbstractFiber.hpp b/emper/AbstractFiber.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..ce028f5b2d4b334bb24cd7479cdf40181dd11bdf
--- /dev/null
+++ b/emper/AbstractFiber.hpp
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright © 2022 Florian Schmaus
+#pragma once
+
+#include <ostream>
+
+#include "emper-common.h"
+
+class AbstractFiber {
+	friend class Dispatcher;
+
+ protected:
+	virtual ~AbstractFiber() = default;
+
+	virtual void run() const = 0;
+
+ public:
+	virtual auto isRunnable() const -> bool { return true; }
+
+	virtual auto getAffinityBuffer() const -> workeraffinity_t* { return nullptr; }
+
+	virtual void printTo(std::ostream& strm, bool withPtr = true) const = 0;
+
+	friend auto operator<<(std::ostream& strm, const AbstractFiber& fiber) -> std::ostream&;
+	friend auto operator<<=(std::ostream& strm, const AbstractFiber& fiber) -> std::ostream&;
+};
diff --git a/emper/Context.hpp b/emper/Context.hpp
index 2a2e77b0372781b65de2ef437b1fd4f31201a193..0ee84b91d41bab29c2eedd96ae36719ebd2861e8 100644
--- a/emper/Context.hpp
+++ b/emper/Context.hpp
@@ -14,9 +14,9 @@
 #include "Debug.hpp"	 // for LOGD, LogSubsystem, LogSubsystem::C, Logger
 #include "Emper.hpp"	 // for Emper::DEBUG
 
+class AbstractFiber;
 class ContextManager;
 class Dispatcher;
-class Fiber;
 
 extern "C" [[noreturn]] void switch_and_load_context(void** toTos);
 // *Not* marked as 'noreturn' because save_and_switch_context does
@@ -35,7 +35,7 @@ class ALIGN_TO_CACHE_LINE Context : Logger<LogSubsystem::C> {
 
 	static thread_local Context* lastContextBeforeReturningToOriginalStack;
 
-	Fiber* currentFiber = nullptr;
+	AbstractFiber* currentFiber = nullptr;
 
 	void* const tos;
 
@@ -60,7 +60,7 @@ class ALIGN_TO_CACHE_LINE Context : Logger<LogSubsystem::C> {
 
 	friend ContextManager;
 
-	auto getFiber() -> Fiber* { return currentFiber; }
+	auto getFiber() -> AbstractFiber* { return currentFiber; }
 
 	/**
 	 * The first function that a newly started context will
@@ -75,7 +75,7 @@ class ALIGN_TO_CACHE_LINE Context : Logger<LogSubsystem::C> {
 
 	friend Dispatcher;
 
-	static void setCurrentFiber(Fiber* fiber) {
+	static void setCurrentFiber(AbstractFiber* fiber) {
 		assert(currentContext);
 
 		currentContext->currentFiber = fiber;
@@ -124,7 +124,7 @@ class ALIGN_TO_CACHE_LINE Context : Logger<LogSubsystem::C> {
 		//		VALGRIND_STACK_DEREGISTER(valgrindStackId);
 	}
 
-	static auto getCurrentFiber() -> Fiber* {
+	static auto getCurrentFiber() -> AbstractFiber* {
 		assert(currentContext);
 
 		return currentContext->currentFiber;
diff --git a/emper/ContextManager.cpp b/emper/ContextManager.cpp
index a25687af03dad82f05a787277462ec4fd5a22e68..367e99de2e31a3a7c0140367c169fca47bdf1d14 100644
--- a/emper/ContextManager.cpp
+++ b/emper/ContextManager.cpp
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// Copyright © 2020-2021 Florian Schmaus
+// Copyright © 2020-2022 Florian Schmaus
 #include "ContextManager.hpp"
 
 #include <cassert>	// for assert
@@ -12,7 +12,7 @@
 #include "emper-common.h"
 #include "emper-config.h"	 // // IWYU pragma: keep
 
-class Fiber;
+class AbstractFiber;
 
 ContextManager::ContextManager(Runtime& runtime) : MemoryManager(runtime), runtime(runtime) {
 	auto newWorkerHook = [this](ATTR_UNUSED workerid_t workerId) {
@@ -87,7 +87,7 @@ void ContextManager::discardAndResume(Context* context) {
 	// Since we are going to discard this context, it will never reach
 	// the end of its dispatch loop, and hence we need to ensure that
 	// its fiber is recycled.
-	Fiber* currentFiber = contextToFree->getFiber();
+	AbstractFiber* currentFiber = contextToFree->getFiber();
 	runtime.dispatcher.recycle(currentFiber);
 
 	contextToFree->discardAndResume(context);
diff --git a/emper/Dispatcher.hpp b/emper/Dispatcher.hpp
index 78d75fff7e166d1212e7b8cf95a748e66dad3ddb..a090eba80c8e4a9640fc3b15ebe14ef1013cca6c 100644
--- a/emper/Dispatcher.hpp
+++ b/emper/Dispatcher.hpp
@@ -1,12 +1,13 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// Copyright © 2020 Florian Schmaus
+// Copyright © 2020-2022 Florian Schmaus
 #pragma once
 
-#include "Common.hpp"			 // for func_t
-#include "Context.hpp"		 // for Context
-#include "Debug.hpp"			 // for LOGD, LogSubsystem, LogSubsystem::DISP
-#include "Fiber.hpp"			 // for Fiber
-#include "emper-common.h"	 // for workeraffinity_t
+#include "AbstractFiber.hpp"
+#include "Common.hpp"
+#include "Context.hpp"
+#include "Debug.hpp"
+#include "Fiber.hpp"
+#include "emper-common.h"
 
 class Runtime;
 class ContextManager;
@@ -24,20 +25,23 @@ class Dispatcher : public Logger<LogSubsystem::DISP> {
 	// The dispatch() method could theoretically be made static in
 	// non-debug builds.
 	// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
-	inline void dispatch(Fiber* fiber) {
+	inline void dispatch(AbstractFiber* fiber) {
 		LOGD("executing fiber " << fiber);
 		Context::setCurrentFiber(fiber);
 		fiber->run();
 	}
 
-	static inline auto isRunnable(Fiber* fiber) -> bool {
+	static inline auto isRunnable(AbstractFiber* abstractFiber) -> bool {
+		auto* fiber = dynamic_cast<Fiber*>(abstractFiber);
+		if (!fiber) return true;
+
 		if (fiber->isMultiFiber()) {
 			return fiber->setRunnableFalse();
 		}
 		return true;
 	}
 
-	static inline auto getAffinityBuffer(Fiber* fiber) -> workeraffinity_t* {
+	static inline auto getAffinityBuffer(AbstractFiber* fiber) -> workeraffinity_t* {
 		return fiber->getAffinityBuffer();
 	}
 
@@ -45,6 +49,15 @@ class Dispatcher : public Logger<LogSubsystem::DISP> {
 		return fiber->doAtomicDecrRefCount();
 	}
 
+	void recycle(AbstractFiber* abstractFiber) {
+		auto* fiber = dynamic_cast<Fiber*>(abstractFiber);
+
+		// We only recycle Fibers.
+		if (!fiber) return;
+
+		recycle(fiber);
+	}
+
 	virtual void recycle(Fiber* fiber) { delete fiber; }
 
  public:
@@ -52,12 +65,12 @@ class Dispatcher : public Logger<LogSubsystem::DISP> {
 
 	virtual ~Dispatcher() = default;
 
-	static auto getCurrentFiber() -> Fiber& {
-		Fiber* fiber = getCurrentFiberPtr();
+	static auto getCurrentFiber() -> AbstractFiber& {
+		AbstractFiber* fiber = getCurrentFiberPtr();
 		return *fiber;
 	}
 
-	static auto getCurrentFiberPtr() -> Fiber* { return Context::getCurrentFiber(); }
+	static auto getCurrentFiberPtr() -> AbstractFiber* { return Context::getCurrentFiber(); }
 
 	static auto isDispatchedControlFlow() -> bool { return getCurrentFiberPtr() != nullptr; }
 
diff --git a/emper/Fiber.cpp b/emper/Fiber.cpp
index 14a6dcc0507d418858b183a5988113db24596594..b5d6a3a1fce9d19c4db013defb4d34c92f7a64c8 100644
--- a/emper/Fiber.cpp
+++ b/emper/Fiber.cpp
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// Copyright © 2020 Florian Schmaus
+// Copyright © 2020-2022 Florian Schmaus
 #include "Fiber.hpp"
 
 #include <iostream>	 // for operator<<, basic_ostream, ostream, basic_ostrea...
@@ -13,19 +13,22 @@ void Fiber::run() const {
 	function(arg);
 }
 
-auto operator<<(std::ostream& strm, const Fiber& fiber) -> std::ostream& {
-	strm << "Fiber [ptr=" << &fiber << " func=" << (fiber.function.target<void(void*)>() != nullptr)
-			 << " arg=" << fiber.arg;
+auto Fiber::isRunnable() const -> bool { return runnable.load(std::memory_order_relaxed); }
 
-	if (fiber.affinity) {
-		strm << " aff=" << fiber.affinity;
+void Fiber::printTo(std::ostream &strm, bool withPtr) const {
+	strm << "Fiber [";
+	if (withPtr) {
+		strm << "ptr=" << this << " ";
+	}
+	// clang-format: off
+	strm << "func=" << (function.target<void(void *)>() != nullptr) << "arg=" << arg;
+	// clang-format: on
+
+	if (affinity) {
+		strm << " aff=" << affinity;
 	} else {
 		strm << " aff=nullptr";
 	}
 
 	strm << "]";
-
-	return strm;
 }
-
-void Fiber::print() const { std::cout << this << std::endl; }
diff --git a/emper/Fiber.hpp b/emper/Fiber.hpp
index 8fd61a78399f4143b4b79f33ffbf17325f76b47b..9da6f9ab1da69d091ce6e41e088a9dc00620235d 100644
--- a/emper/Fiber.hpp
+++ b/emper/Fiber.hpp
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// Copyright © 2020-2021 Florian Schmaus
+// Copyright © 2020-2022 Florian Schmaus
 #pragma once
 
 #include <atomic>				// for atomic_uint, atomic, __atomic_base, memory...
@@ -9,6 +9,7 @@
 #include <type_traits>	// for remove_reference<>::type // IWYU pragma: keep
 #include <utility>
 
+#include "AbstractFiber.hpp"
 #include "Common.hpp"			 // for ALIGN_TO_CACHE_LINE
 #include "Debug.hpp"			 // for LogSubsystem, LogSubsystem::F, Logger
 #include "emper-common.h"	 // for workeraffinity_t, UNUSED_ARG
@@ -25,7 +26,7 @@ class MpscQueue;
 #define FIBER_FUN_TEMPLATE_ARG void(void*)
 #define FIBER_FUN0_TEMPLATE_ARG void(void)
 
-class ALIGN_TO_CACHE_LINE Fiber : public Logger<LogSubsystem::F> {
+class ALIGN_TO_CACHE_LINE Fiber : public AbstractFiber, public Logger<LogSubsystem::F> {
  public:
 	using fiber_fun_t = std::function<void(void*)>;
 	using fiber_fun0_t = std::function<void()>;
@@ -125,9 +126,19 @@ class ALIGN_TO_CACHE_LINE Fiber : public Logger<LogSubsystem::F> {
 		return *affinity;
 	}
 
-	void print() const;
+	void printTo(std::ostream& strm, bool withPtr) const;
 
-	[[nodiscard]] auto isRunnable() const -> bool { return runnable; }
+	/**
+	 * @brief check if this Fiber is runnable.
+	 *
+	 * Checks if this Fiber can be run, i.e. if it still needs to be
+	 * run. Note that with multi Fibers, a positive result, i.e., if
+	 * this function returns 'true', may be outdated immediately due to
+	 * concurrency.
+	 *
+	 * @return 'true' if this fiber is runnable, 'false' otherwhise.
+	 */
+	[[nodiscard]] auto isRunnable() const -> bool;
 
 	[[nodiscard]] auto isMultiFiber() const -> bool { return isMulti; }
 
diff --git a/emper/NextFiberResult.hpp b/emper/NextFiberResult.hpp
index 9b927e14f95870acca971bdc3de0dcfc8350aa82..b3d021d32d621169e30d33d1520ab8bbfb8c0a19 100644
--- a/emper/NextFiberResult.hpp
+++ b/emper/NextFiberResult.hpp
@@ -1,14 +1,14 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// Copyright © 2021 Florian Schmaus
+// Copyright © 2021-2022 Florian Schmaus
 #pragma once
 
 #include <cstdint>
 
 #include "FiberSource.hpp"
 
-class Fiber;
+class AbstractFiber;
 
 struct NextFiberResult {
-	Fiber* const fiber;
+	AbstractFiber* fiber;
 	const emper::FiberSource source;
 };
diff --git a/emper/Scheduler.hpp b/emper/Scheduler.hpp
index 57204e267655036ededee7b963fdf78132df6589..665064d8a838ff0ec155f7340b083a6291f80051 100644
--- a/emper/Scheduler.hpp
+++ b/emper/Scheduler.hpp
@@ -16,6 +16,7 @@
 #include "emper-common.h"	 // for workeraffinity_t
 #include "lib/adt/LockedUnboundedQueue.hpp"
 
+class AbstractFiber;
 class Runtime;
 class RuntimeStrategy;
 struct NextFiberResult;
@@ -74,7 +75,7 @@ class Scheduler : public Logger<LogSubsystem::SCHED> {
 	virtual void scheduleFromAnywhereInternal(Fiber& fiber) = 0;
 	virtual void scheduleFromAnywhereInternal(Fiber** fibers, unsigned count) = 0;
 
-	void recycle(Fiber* fiber) { dispatcher.recycle(fiber); };
+	void recycle(AbstractFiber* fiber) { dispatcher.recycle(fiber); };
 
 	virtual auto nextFiber() -> std::optional<NextFiberResult> = 0;
 
diff --git a/emper/meson.build b/emper/meson.build
index 4423f03b37b1b5adfc76f57f0d7201fcdf27ade9..876f76f4cc649b7468a1901680068ecfccadb3a2 100644
--- a/emper/meson.build
+++ b/emper/meson.build
@@ -13,6 +13,7 @@ nasm_gen = generator(nasm,
 emper_asm_objects = nasm_gen.process(emper_asm_sources)
 
 emper_cpp_sources = [
+  'AbstractFiber.cpp',
   'CallerEnvironment.cpp',
   'Runtime.cpp',
   'Emper.cpp',
diff --git a/emper/strategies/AbstractWorkStealingScheduler.cpp b/emper/strategies/AbstractWorkStealingScheduler.cpp
index 2814a4173a3c64aece657d2239d915094a8c9e91..47a89a7a78b6973250895fc550986dfdde044e68 100644
--- a/emper/strategies/AbstractWorkStealingScheduler.cpp
+++ b/emper/strategies/AbstractWorkStealingScheduler.cpp
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// Copyright © 2021 Florian Schmaus, Florian Fischer
+// Copyright © 2021-2022 Florian Schmaus, Florian Fischer
 #include "AbstractWorkStealingScheduler.hpp"
 
 #include <algorithm>
@@ -9,6 +9,7 @@
 #include <ostream>	// for operator<<, basic_ostream<>::__ostream_type
 #include <vector>
 
+#include "AbstractFiber.hpp"
 #include "CallerEnvironment.hpp"
 #include "Common.hpp"	 // for unlikely, likely
 #include "Debug.hpp"	 // for ABORT
@@ -78,7 +79,7 @@ void AbstractWorkStealingScheduler::scheduleToMpscQueue(Fiber& fiber, workerid_t
 	onNewWork<CallerEnvironment::EMPER>(emper::FiberHint{workerId, emper::FiberSource::mpscQueue});
 }
 
-auto AbstractWorkStealingScheduler::maybeRecycle(Fiber* fiber) -> bool {
+auto AbstractWorkStealingScheduler::maybeRecycle(AbstractFiber* fiber) -> bool {
 	if (fiber->isRunnable()) return false;
 
 	recycle(fiber);
@@ -145,7 +146,7 @@ auto AbstractWorkStealingScheduler::nextFiberViaAnywhereQueue() -> std::optional
 auto AbstractWorkStealingScheduler::tryStealFiberFrom(workerid_t victim)
 		-> std::optional<NextFiberResult> {
 	constexpr int maxRetries = emper::WAITFREE_WORK_STEALING ? 0 : -1;
-	Fiber* fiber;
+	AbstractFiber* fiber;
 popTop:
 	StealingResult res = queues[victim]->popTop<maxRetries>(&fiber);
 	if (res == StealingResult::Stolen) {
@@ -170,7 +171,7 @@ popTop:
 
 auto AbstractWorkStealingScheduler::nextFiberResultViaWorkStealing()
 		-> std::optional<NextFiberResult> {
-	Fiber* fiber;
+	AbstractFiber* fiber;
 
 popBottom:
 	bool poped = queue.popBottom(&fiber);
diff --git a/emper/strategies/AbstractWorkStealingScheduler.hpp b/emper/strategies/AbstractWorkStealingScheduler.hpp
index 7dd3198bf0b5b70b958fbe67ad3be26e39f41c0a..b452edeff6070d515215e4d4e4b9252c90c47137 100644
--- a/emper/strategies/AbstractWorkStealingScheduler.hpp
+++ b/emper/strategies/AbstractWorkStealingScheduler.hpp
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// Copyright © 2021 Florian Schmaus
+// Copyright © 2021-2022 Florian Schmaus
 #pragma once
 
 #include <cstddef>	// for size_t
@@ -17,15 +17,16 @@
 #endif
 
 struct NextFiberResult;
+class AbstractFiber;
 class Runtime;
 class RuntimeStrategy;
 
 class AbstractWorkStealingScheduler : public Scheduler {
 	template <size_t SIZE>
 #ifdef EMPER_LOCKED_WS_QUEUE
-	using WsQueue = adt::LockedQueue<Fiber*, SIZE>;
+	using WsQueue = adt::LockedQueue<AbstractFiber*, SIZE>;
 #else
-	using WsQueue = adt::WsClQueue<Fiber*, SIZE>;
+	using WsQueue = adt::WsClQueue<AbstractFiber*, SIZE>;
 #endif
 	using MpscQueue = adt::MpscQueue<Fiber>;
 
@@ -46,7 +47,7 @@ class AbstractWorkStealingScheduler : public Scheduler {
 	void scheduleViaWorkStealing(Fiber& fiber);
 	void scheduleToMpscQueue(Fiber& fiber, workerid_t workerId);
 
-	auto maybeRecycle(Fiber* fiber) -> bool;
+	auto maybeRecycle(AbstractFiber* fiber) -> bool;
 
 	// This method is static because it only uses the thread_local mpscQueue
 	static auto nextFiberResultFromMpscQueue() -> std::optional<NextFiberResult>;
diff --git a/emper/strategies/laws/LawsDispatcher.cpp b/emper/strategies/laws/LawsDispatcher.cpp
index 7980f7c732cec4f466648c0645dfcff62e7f423a..47dfc389d1be685bb10c8da030b042ba8289b6a5 100644
--- a/emper/strategies/laws/LawsDispatcher.cpp
+++ b/emper/strategies/laws/LawsDispatcher.cpp
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// Copyright © 2020-2021 Florian Schmaus
+// Copyright © 2020-2022 Florian Schmaus
 #include "LawsDispatcher.hpp"
 
 #include <optional>
@@ -14,6 +14,8 @@
 #include "emper-common.h"
 #include "strategies/laws/LawsWorkerStats.hpp"
 
+class AbstractFiber;
+
 void LawsDispatcher::recycle(Fiber* fiber) {
 	// If the ref count has not reached zero yet, do not recycle the
 	// fiber. But only if the fiber is a multi fiber, i.e. was placed
@@ -33,7 +35,7 @@ void LawsDispatcher::dispatchLoop() {
 			continue;
 		}
 
-		Fiber* const fiber = next->fiber;
+		AbstractFiber* fiber = next->fiber;
 
 		// The isRunnable() method performes an atomic swap on a boolean,
 		// which was initialized to true, in order to check if this fiber
@@ -78,6 +80,6 @@ void LawsDispatcher::dispatchLoop() {
 			dispatch(fiber);
 		}
 
-		recycle(fiber);
+		Dispatcher::recycle(fiber);
 	}
 }
diff --git a/emper/strategies/ws/WsDispatcher.cpp b/emper/strategies/ws/WsDispatcher.cpp
index 7826f33a82d0628091dbccaf38c6cca90bcb61f9..90a16f6f6d6b8229e64d821c1c570144c28ef4a4 100644
--- a/emper/strategies/ws/WsDispatcher.cpp
+++ b/emper/strategies/ws/WsDispatcher.cpp
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// Copyright © 2020-2022 Florian Schmaus Florian Fischer
+// Copyright © 2020-2022 Florian Schmaus, Florian Fischer
 #include "WsDispatcher.hpp"
 
 #include <optional>
@@ -7,7 +7,7 @@
 #include "NextFiberResult.hpp"
 #include "Runtime.hpp"	// for Runtime
 
-class Fiber;
+class AbstractFiber;
 
 void WsDispatcher::dispatchLoop() {
 	while (true) {
@@ -17,7 +17,7 @@ void WsDispatcher::dispatchLoop() {
 
 			continue;
 		}
-		Fiber* const fiber = next->fiber;
+		AbstractFiber* fiber = next->fiber;
 
 		dispatch(fiber);