diff --git a/benches/barbershop.rs b/benches/barbershop.rs
index a8ab06fd9727a13bc90ecd91980ff4f48a11762c..9d239f801d0b5830e9b02277fa7d1a7c5894e26e 100644
--- a/benches/barbershop.rs
+++ b/benches/barbershop.rs
@@ -1,7 +1,8 @@
-use simcore_rs::{Time, SimContext, Facility, simulation};
+use simcore_rs::{Time, SimContext, Facility, simulation, Process};
 use rand::{distributions::Uniform, rngs::SmallRng, SeedableRng, Rng};
 use criterion::{Criterion, BenchmarkId, criterion_group, criterion_main, BatchSize};
 use std::time::Duration;
+use std::rc::Rc;
 
 const SEED_A   :  u64 = 100000;
 const SEED_S   :  u64 = 200000;
@@ -22,17 +23,15 @@ fn barbershop(c: &mut Criterion) {
 			&stop_time,
 			|b, &stop_time|
 				b.iter_batched(
-					|| {
-						(SmallRng::from_rng(&mut rng_a).unwrap(),
-						 SmallRng::from_rng(&mut rng_s).unwrap())
-					},
-					|(rng_a, rng_s)| {
-						let shop = BarberShop {
-							stop_time, rng_a, rng_s, joe: &Facility::new()
-						};
-						
-						simulation(|sim| shop.actions(sim));
+					|| BarberShop {
+						stop_time,
+						rng_a: SmallRng::from_rng(&mut rng_a).unwrap(),
+						rng_s: SmallRng::from_rng(&mut rng_s).unwrap(),
+						joe: Rc::new(Facility::new())
 					},
+					|shop| simulation(
+						|sim| Process::new(shop.actions(sim))
+					),
 					BatchSize::SmallInput
 				)
 		);
@@ -47,16 +46,17 @@ criterion_main!(benches);
 /* *************************** Barbershop Example *************************** */
 
 /// Barbershop process.
-struct BarberShop<'j> {
-	stop_time: Time, rng_a: SmallRng, rng_s: SmallRng, joe: &'j Facility
+struct BarberShop {
+	stop_time: Time, rng_a: SmallRng, rng_s: SmallRng, joe: Rc<Facility>
 }
 
-impl<'j> BarberShop<'j> {
-	async fn actions(self, sim: SimContext<'j>) {
+impl BarberShop {
+	async fn actions(self, sim: SimContext<'_>) {
 		// unpack the barber shop structure for easier access
 		let Self {
 			stop_time, mut rng_a, mut rng_s, joe
 		} = self;
+		let joe2 = joe.clone();
 		
 		// activate a process to generate the customers
 		sim.activate(async move {
@@ -69,7 +69,8 @@ impl<'j> BarberShop<'j> {
 			while sim.now() < stop_time {
 				// activate the next customer
 				sim.activate(Customer {
-					joe, rng: SmallRng::from_seed(rng_s.gen())
+					joe: joe.clone(),
+					rng: SmallRng::from_seed(rng_s.gen())
 				}.actions(sim));
 				// wait some time before activating the next customer
 				sim.advance(rng_a.sample(dist)).await;
@@ -80,14 +81,14 @@ impl<'j> BarberShop<'j> {
 		sim.advance(self.stop_time).await;
 		
 		// finish processing the queue (no more customers arrive)
-		joe.seize().await;
+		joe2.seize().await;
 	}
 }
 
 /// Customer process with access to the barber and a random number generator.
-struct Customer<'j> { joe: &'j Facility, rng: SmallRng }
+struct Customer { joe: Rc<Facility>, rng: SmallRng }
 
-impl Customer<'_> {
+impl Customer {
 	pub async fn actions(mut self, sim: SimContext<'_>) {
 		// access the barber
 		self.joe.seize().await;
diff --git a/src/bin/barbershop.rs b/src/bin/barbershop.rs
index bceb8b328e559585be9deb03caf2ff453aaa98d6..d90e2eaedd76082b6e66e09717d74328bdf53e02 100644
--- a/src/bin/barbershop.rs
+++ b/src/bin/barbershop.rs
@@ -1,4 +1,4 @@
-use simcore_rs::{Time, SimContext, Facility, RandomVar, simulation};
+use simcore_rs::{Process, Time, SimContext, Facility, RandomVar, simulation};
 use rand::{distributions::Uniform, rngs::SmallRng, SeedableRng, Rng};
 
 // helper constants
@@ -16,7 +16,7 @@ fn main() {
 	let rv = &RandomVar::new();
 	
 	// the main process
-	simulation(|sim| async move {
+	simulation(|sim| Process::new(async move {
 		// activate a process to generate the customers
 		sim.activate(async move {
 			let dist = Uniform::new(12.0, 24.0);
@@ -28,7 +28,9 @@ fn main() {
 			while sim.now() < STOP_TIME {
 				// activate the next customer
 				sim.activate(Customer {
-					joe, rv, rng: SmallRng::from_seed(rng_s.gen())
+					joe: joe.clone(),
+					rv: rv.clone(),
+					rng: SmallRng::from_seed(rng_s.gen())
 				}.actions(sim));
 				
 				// wait some time before activating the next customer
@@ -41,16 +43,16 @@ fn main() {
 		
 		// finish processing the queue (no more customers arrive)
 		joe.seize().await;
-	});
+	}));
 	
 	println!("Stats: {:.3}", rv);
 }
 
 /// Customer process with access to the barber and a random number generator.
-struct Customer<'j> { joe: &'j Facility, rv: &'j RandomVar, rng: SmallRng }
+struct Customer<'c> { joe: &'c Facility, rv: &'c RandomVar, rng: SmallRng }
 
-impl Customer<'_> {
-	pub async fn actions(mut self, sim: SimContext<'_>) {
+impl<'c> Customer<'c> {
+	pub async fn actions(mut self, sim: SimContext<'c>) {
 		// access the barber and record the time for the report
 		let time = sim.now();
 		self.joe.seize().await;
diff --git a/src/lib.rs b/src/lib.rs
index 359e6fdac942fe19bd3ac23853943471e1c1305d..8abc45c797655117c0d2a341ac97abbeb36d505d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -35,43 +35,31 @@ struct Scheduler<'s> {
 pub struct SimContext<'s> { handle: *const Scheduler<'s> }
 
 /// Performs a single simulation run.
-/// Input is a function that takes a simulation context and returns the future
-/// that is used to initialize the first process.
-pub fn simulation<'s,G,F>(main: G)
-	where G: FnOnce(SimContext<'s>) -> F,
-	      F: Future<Output = ()> + 's
-{
+/// 
+/// Input is a function that takes a simulation context and returns the first
+/// process. It would be better if the function only needed to return the future
+/// needed to initialize the first process, but then the function signature gets
+/// more complicated and is harder to explain in a paper.
+pub fn simulation<'s>(main: impl FnOnce(SimContext<'s>) -> Process<'s>) {
 	// (this function's signature could be better but there seems to be a
 	//  bug with higher-order trait-bounds right now that prevents me from
 	//  referring to the same lifetime in multiple type parameters)
-	use std::task::{Waker, RawWaker};
 	
 	// create a fresh scheduler and a handle to it
 	let sched = Scheduler::new();
 	let sim = SimContext { handle: &sched };
 	
 	// construct a custom context to pass into the poll()-method
-	let event_waker = Box::leak(Box::new(StateEventWaker {
-		process: Process::default(),
-		context: sim
-	}));
-	let waker = unsafe {
-		Waker::from_raw(RawWaker::new(
-			event_waker as *const _ as *const (),
-			&WAKER_VTABLE
-		))
-	};
+	let event_waker = StateEventWaker::new(sim);
+	let waker = unsafe { event_waker.as_waker() };
 	let mut cx = Context::from_waker(&waker);
 	
 	// evaluate the passed function for the main process and schedule it
-	let root = Process::new(main(sim));
+	let root = main(sim);
 	sched.schedule(root.clone());
 	
 	// pop processes until empty or the main process terminates
 	while let Some(process) = sched.next_event() {
-		// register the next process with the context
-		event_waker.process = process.clone();
-		
 		if process.poll(&mut cx).is_ready() && process == root { break; }
 	}
 	
@@ -225,23 +213,85 @@ impl Ord for NextEvent<'_> {
 
 /* **************************** specialized waker *************************** */
 
-#[derive(Clone)]
 /// Complex waker that is used to implement state events.
-struct StateEventWaker<'s> {
+/// 
+/// This is the shallow version that is created on the stack of the function
+/// running the event loop. It assumes that the stored process is the currently
+/// active one and creates the deep version when it is cloned.
+struct StateEventWaker<'s> { context: SimContext<'s> }
+
+/// Deep version of the complex waker used to implement state events.
+/// 
+/// This version actually holds a process as well as a simulation context on the
+/// heap. Dropping the waker releases the memory.
+struct StateEventWakerDeep<'s> {
 	process: Process<'s>,
 	context: SimContext<'s>
 }
 
-impl StateEventWaker<'_> {
+impl<'s> StateEventWaker<'s> {
+	/// Creates a new (shallow) waker using only the simulation context.
+	fn new(sim: SimContext<'s>) -> Self {
+		StateEventWaker { context: sim }
+	}
+	
+	/// Constructs a new waker using only a reference.
+	/// 
+	/// This function is unsafe because it is up to the user to ensure that the
+	/// waker doesn't outlive the reference.
+	unsafe fn as_waker(&self) -> task::Waker {
+		task::Waker::from_raw(
+			task::RawWaker::new(
+				self as *const _ as *const (),
+				&WAKER_VTABLE
+			)
+		)
+	}
+	
+	/// Creates a deep waker from a reference.
+	/// 
+	/// This function is safe because it allocates memory on the heap.
+	fn deep_waker(&self) -> task::RawWaker {
+		StateEventWakerDeep::raw_waker(
+			self.context, self.context.active()
+		)
+	}
+	
 	unsafe fn clone(this: *const ()) -> task::RawWaker {
-		let waker = &*(this as *const Self);
+		(&*(this as *const Self)).deep_waker()
+	}
+	
+	unsafe fn wake(_this: *const ()) {
+		unreachable!("attempting to awake the active process")
+	}
+	
+	unsafe fn wake_by_ref(_this: *const ()) {
+		unreachable!("attempting to awake the active process")
+	}
+	
+	unsafe fn drop(_this: *const ()) {
+		// memory released in the main event loop
+	}
+}
+
+impl<'s> StateEventWakerDeep<'s> {
+	/// Constructs a raw waker from a simulation context and a process.
+	fn raw_waker(sim: SimContext<'s>, process: Process<'s>) -> task::RawWaker {
+		let waker = Box::leak(Box::new(
+			Self { process, context: sim }
+		));
 		
 		task::RawWaker::new(
-			Box::into_raw(Box::new(waker.clone())) as *const (),
-			&WAKER_VTABLE
+			waker as *const _ as *const (),
+			&WAKER_DEEP_VTABLE
 		)
 	}
 	
+	unsafe fn clone(this: *const ()) -> task::RawWaker {
+		let waker = &*(this as *const Self);
+		Self::raw_waker(waker.context, waker.process.clone())
+	}
+	
 	unsafe fn wake(this: *const ()) {
 		let waker = Box::from_raw(this as *const Self as *mut Self);
 		waker.context.reactivate(waker.process);
@@ -265,6 +315,14 @@ static WAKER_VTABLE: task::RawWakerVTable = task::RawWakerVTable::new(
 	StateEventWaker::drop
 );
 
+/// Virtual function table for the deep waker.
+static WAKER_DEEP_VTABLE: task::RawWakerVTable = task::RawWakerVTable::new(
+	StateEventWakerDeep::clone,
+	StateEventWakerDeep::wake,
+	StateEventWakerDeep::wake_by_ref,
+	StateEventWakerDeep::drop
+);
+
 /* *************************** specialized futures ************************** */
 
 /// Future that blocks on the first call and returns on the second one.