diff --git a/emper/Actor.hpp b/emper/Actor.hpp
index 2a7bf281a3561903d42907aeaf30491e7b2a34f9..4065ee2c8ff74916ede9e851126249424d8f1431 100644
--- a/emper/Actor.hpp
+++ b/emper/Actor.hpp
@@ -57,7 +57,7 @@ class Actor {
 	void stop() { setState(Stopped); }
 
  public:
-	virtual ~Actor() = default;
+	virtual ~Actor() { stop(); }
 
 	template <CallerEnvironment callerEnvironment = CallerEnvironment::EMPER>
 	void start() {
diff --git a/emper/Runtime.cpp b/emper/Runtime.cpp
index af097e0f731dace5057d76f7ebd74e324f1db72b..51f87515a17395e18036695431d19478804af675 100644
--- a/emper/Runtime.cpp
+++ b/emper/Runtime.cpp
@@ -67,6 +67,7 @@ using emper::io::IoContext;
 Runtime::Runtime(workerid_t workerCount, RuntimeStrategyFactory& strategyFactory, unsigned int seed)
 		: workerCount(workerCount),
 			workerLatch(workerCount),
+			workerThreadExitLatch(workerCount),
 			strategy(strategyFactory.constructRuntimeStrategy(*this)),
 			scheduler(strategy->getScheduler()),
 			dispatcher(strategy->getDispatcher()),
@@ -229,6 +230,12 @@ auto Runtime::workerLoop(Worker* worker) -> void* {
 
 	// Threads return here if Context::switchToOriginalStack() is called.
 
+	// Ensure that all worker threads exit "at the same" time. Otherwise
+	// it would be possible that one thread is work-stealing,
+	// potentially accessing a work stealing queue of an worker thread
+	// that already exited, causing an invalid access.
+	workerThreadExitLatch.count_down_and_wait();
+
 	return nullptr;
 }
 
diff --git a/emper/Runtime.hpp b/emper/Runtime.hpp
index 24a8c52adc5126119cba00adf683a926f12e862e..621eda413c7a46cd1121d1845b6e25646b873ecf 100644
--- a/emper/Runtime.hpp
+++ b/emper/Runtime.hpp
@@ -51,6 +51,7 @@ class Runtime : public Logger<LogSubsystem::RUNTI> {
 	std::vector<std::function<void(workerid_t)>> newWorkerHooks;
 
 	Latch workerLatch;
+	Latch workerThreadExitLatch;
 
 	RuntimeStrategy* const strategy;
 	Scheduler& scheduler;
diff --git a/tests/AlarmFutureTest.cpp b/tests/AlarmFutureTest.cpp
index b14830a48c871b63470181f5b8c21e544e8c4267..7ff4e6f278b658633c37784b1129ceb56616ad19 100644
--- a/tests/AlarmFutureTest.cpp
+++ b/tests/AlarmFutureTest.cpp
@@ -4,7 +4,6 @@
 #include <cerrno>		// for ETIME
 #include <chrono>		// for microseconds, duration_cast, operator-
 #include <cstdint>	// for int32_t
-#include <cstdlib>	// for exit, EXIT_SUCCESS
 
 #include "emper-config.h"
 #include "io/Future.hpp"	// for AlarmFuture
@@ -28,6 +27,4 @@ void emperTest() {
 
 	assert(std::chrono::duration_cast<std::chrono::microseconds>(end - start) >=
 				 std::chrono::seconds(1));
-
-	exit(EXIT_SUCCESS);
 }
diff --git a/tests/CancelFutureTest.cpp b/tests/CancelFutureTest.cpp
index 21eaa79fa71d56267be77fe6a907a1fa5f6e5809..0ff9b09f38367b7a8b37a2ebd9662472847fb371 100644
--- a/tests/CancelFutureTest.cpp
+++ b/tests/CancelFutureTest.cpp
@@ -5,7 +5,6 @@
 #include <cassert>	// for assert
 #include <cerrno>		// for ECANCELED, ETIME
 #include <cstdint>	// for uint64_t, int32_t
-#include <cstdlib>	// for exit, EXIT_SUCCESS
 
 #include "Common.hpp"			// for DIE_MSG_ERRNO
 #include "io/Future.hpp"	// for ReadFuture, WriteFuture
@@ -57,6 +56,4 @@ void emperTest() {
 	int r = readFuture2.cancel();
 	assert(r == -EINTR || r == -ECANCELED);
 	assert(readFuture.wait() == sizeof(write_buf) && read_buf == write_buf);
-
-	exit(EXIT_SUCCESS);
 }
diff --git a/tests/IncrementalCompletionTest.cpp b/tests/IncrementalCompletionTest.cpp
index 229ce1783c013b0ba8b63e31fc8e3c017da3fd61..0f102fd63230f127e388e008ba55e6e420bdf593 100644
--- a/tests/IncrementalCompletionTest.cpp
+++ b/tests/IncrementalCompletionTest.cpp
@@ -48,6 +48,4 @@ void emperTest() {
 
 	delete[] memOut;
 	delete[] memIn;
-
-	exit(EXIT_SUCCESS);
 }
diff --git a/tests/LinkFutureTest.cpp b/tests/LinkFutureTest.cpp
index b9859ec5c4bc132641058518cca02e62b1633e91..f0f2d04df8fe8a2b69d509715bc881d901145ea8 100644
--- a/tests/LinkFutureTest.cpp
+++ b/tests/LinkFutureTest.cpp
@@ -8,7 +8,6 @@
 #include <cassert>	// for assert
 #include <cerrno>		// for EBADF, ECANCELED
 #include <cstdint>	// for uint64_t, int32_t
-#include <cstdlib>	// for exit, EXIT_SUCCESS
 
 #include "Common.hpp"	 // for DIE_MSG_ERRNO, DIE_MSG
 #include "io.hpp"
@@ -126,6 +125,4 @@ void emperTest() {
 	successLoop();
 	failureChainInvCor();
 	failureChainCorInvCor();
-
-	exit(EXIT_SUCCESS);
 }
diff --git a/tests/ReuseBpsTest.cpp b/tests/ReuseBpsTest.cpp
index 82d8a06aea05509f2d38fd21780b2f090fb3c1a9..7eddfb9174917198c06e6b66c6a0608ccb8139b2 100644
--- a/tests/ReuseBpsTest.cpp
+++ b/tests/ReuseBpsTest.cpp
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// Copyright © 2020 Florian Schmaus
-#include <cstdlib>	// for exit, EXIT_SUC...
-
+// Copyright © 2020-2021 Florian Schmaus
 #include "BinaryPrivateSemaphore.hpp"		 // for BPS
 #include "CountingPrivateSemaphore.hpp"	 // for CPS
 #include "emper.hpp"										 // for spawn
@@ -34,6 +32,4 @@ void emperTest() {
 
 	// Wait for the fibers to finish.
 	cps.wait();
-
-	exit(EXIT_SUCCESS);
 }
diff --git a/tests/ReuseFutureTest.cpp b/tests/ReuseFutureTest.cpp
index 7bc930c8885ee7872a2f323fa60a23a3b4bd9472..f08dccf0a69a598fa30bcaca8fd8831cc9dc7d99 100644
--- a/tests/ReuseFutureTest.cpp
+++ b/tests/ReuseFutureTest.cpp
@@ -4,7 +4,6 @@
 
 #include <cerrno>		// for errno
 #include <cstdint>	// for uint64_t, int32_t
-#include <cstdlib>	// for exit, EXIT_SUCCESS
 
 #include "Common.hpp"										 // for DIE_MSG_ERRNO
 #include "CountingPrivateSemaphore.hpp"	 // for CPS
@@ -38,8 +37,10 @@ void emperTest() {
 						DIE_MSG_ERRNO("read failed");
 					}
 
-					// reset the BPS used to signal the completion of this future
-					read_future.reset();
+					if (i != ITERATIONS - 1) {
+						// reset the BPS used to signal the completion of this future
+						read_future.reset();
+					}
 				}
 			},
 			cps);
@@ -53,14 +54,14 @@ void emperTest() {
 						DIE_MSG_ERRNO("write failed");
 					}
 
-					// reset the BPS used to signal the completion of this future
-					write_future.reset();
+					if (i != ITERATIONS - 1) {
+						// reset the BPS used to signal the completion of this future
+						write_future.reset();
+					}
 				}
 			},
 			cps);
 
 	// Wait for the fibers to finish.
 	cps.wait();
-
-	exit(EXIT_SUCCESS);
 }
diff --git a/tests/TellActorFromAnywhereTest.cpp b/tests/TellActorFromAnywhereTest.cpp
index 70800ac153e3fa9fce342659631138f7a75d805c..3e63f1f1a4b9c49d41d6bb11a5c00d093886bfcb 100644
--- a/tests/TellActorFromAnywhereTest.cpp
+++ b/tests/TellActorFromAnywhereTest.cpp
@@ -18,14 +18,16 @@ class SignallingActor : public Actor<unsigned int> {
 };
 
 void emperTest() {
-	BinaryPrivateSemaphore bps;
-	SignallingActor signallingActor(bps);
-	signallingActor.start();
+	// Heap allocate the Actor and the BPS until we have a way to
+	// cleanly terminate the Actor.
+	auto* bps = new BinaryPrivateSemaphore();
+	auto* signallingActor = new SignallingActor(*bps);
+	signallingActor->start();
 
 	// TODO: Use std::jthread once EMPER uses C++20.
-	std::thread signallingThread([&] { signallingActor.tellFromAnywhere(1); });
+	std::thread signallingThread([&] { signallingActor->tellFromAnywhere(1); });
 
-	bps.wait();
+	bps->wait();
 
 	// TODO: Remove this once we use std::jthread when EMPER uses C++20.
 	signallingThread.join();
diff --git a/tests/TimeoutWrapperTest.cpp b/tests/TimeoutWrapperTest.cpp
index 8211b3766d8ccac16d1c4c632395e5cf91d39e81..ef7e3b36eda94e864ace3c20ac47d4935dce47ea 100644
--- a/tests/TimeoutWrapperTest.cpp
+++ b/tests/TimeoutWrapperTest.cpp
@@ -5,7 +5,6 @@
 #include <cassert>	// for assert
 #include <cerrno>		// for ECANCELED, ETIME
 #include <cstdint>	// for uint64_t, int32_t
-#include <cstdlib>	// for exit, EXIT_SUCCESS
 
 #include "Common.hpp"			// for DIE_MSG_ERRNO
 #include "io/Future.hpp"	// for ReadFuture, TimeoutWrapper
@@ -30,6 +29,4 @@ void emperTest() {
 
 	res = readFuture.wait();
 	assert(res == -ECANCELED);
-
-	exit(EXIT_SUCCESS);
 }
diff --git a/tests/TooLongFutureChain.cpp b/tests/TooLongFutureChain.cpp
index 8f861e0a6cb0d82c7754ea2f9ea8930f7844f5db..7a1bb3ea49a6e3b3e38d2c641f536751025fee1e 100644
--- a/tests/TooLongFutureChain.cpp
+++ b/tests/TooLongFutureChain.cpp
@@ -23,5 +23,4 @@ void emperTest() {
 	}
 
 	futures[links - 1]->submitAndWait();
-	exit(EXIT_SUCCESS);
 }
diff --git a/tests/YieldToAnywhereTest.cpp b/tests/YieldToAnywhereTest.cpp
index ee0fb8ef89552e30b4119c41e0017e25494e0a47..e9248ca166f338e110694278a83eb3a25e71d8b5 100644
--- a/tests/YieldToAnywhereTest.cpp
+++ b/tests/YieldToAnywhereTest.cpp
@@ -1,7 +1,5 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// Copyright © 2020 Florian Schmaus
-#include <cstdlib>
-
+// Copyright © 2020-2021 Florian Schmaus
 #include "CountingPrivateSemaphore.hpp"
 #include "emper.hpp"
 
@@ -18,5 +16,4 @@ void emperTest() {
 				cps);
 	}
 	cps.wait();
-	exit(EXIT_SUCCESS);
 }
diff --git a/tests/test-runner/test-runner.cpp b/tests/test-runner/test-runner.cpp
index b78c7615eeda81ebc88ab6b7e81967cd02de896f..ca3b1d64628764806fa95c915e4ea31980131ec7 100644
--- a/tests/test-runner/test-runner.cpp
+++ b/tests/test-runner/test-runner.cpp
@@ -7,11 +7,12 @@
 
 #include "Fiber.hpp"
 #include "Runtime.hpp"
+#include "lib/sync/Semaphore.hpp"
 
-void invokeTest() {
+static void invokeTest(emper::lib::sync::Semaphore* successSem) {
 	emperTest();
 
-	exit(EXIT_SUCCESS);
+	successSem->notify();
 }
 
 auto testMain() -> int {
@@ -21,12 +22,13 @@ auto testMain() -> int {
 	}
 
 	Runtime runtime;
+	emper::lib::sync::Semaphore successSem;
 
-	Fiber* alphaFiber = Fiber::from(&invokeTest);
+	Fiber* alphaFiber = Fiber::from([&successSem] { invokeTest(&successSem); });
 
 	runtime.scheduleFromAnywhere(*alphaFiber);
 
-	runtime.waitUntilFinished();
+	successSem.wait();
 
-	return EXIT_FAILURE;
+	return EXIT_SUCCESS;
 }
diff --git a/tests/test-runner/test-runner.hpp b/tests/test-runner/test-runner.hpp
index 3819bafcee771741d98fbaafd14fbaa160658cc8..a871918c09ab756bbb87bdf1ba652f513823884b 100644
--- a/tests/test-runner/test-runner.hpp
+++ b/tests/test-runner/test-runner.hpp
@@ -3,6 +3,4 @@
 #pragma once
 void emperTest() __attribute__((weak));
 
-void invokeTest();
-
 auto testMain() -> int;