Skip to main content

cubecl_environment/stream/
handle.rs

1use core::future::Future;
2use core::pin::Pin;
3use core::task::{Context, Poll};
4
5use super::StreamId;
6
7/// A manually managed stream identity.
8///
9/// A [`Stream`] is a pure identity: it owns no backend resources (backend
10/// streams are pool-managed by the runtimes) and is freely copyable. Use it to
11/// pin work to a stable stream regardless of which thread or task executes it:
12///
13/// - [`Stream::enter`] runs synchronous work on the stream.
14/// - [`Stream::attach`] binds a future to the stream, surviving executor
15///   work-stealing on any async runtime.
16/// - [`Stream::spawn`] runs a closure on a fresh OS thread bound to a fresh
17///   stream (native std only).
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct Stream {
20    id: StreamId,
21}
22
23impl Stream {
24    /// Creates a new stream with a freshly allocated identity.
25    #[allow(clippy::new_without_default)]
26    pub fn new() -> Self {
27        Self {
28            id: StreamId::allocate(),
29        }
30    }
31
32    /// Adopts an existing stream id.
33    pub const fn from_id(id: StreamId) -> Self {
34        Self { id }
35    }
36
37    /// The underlying stream id.
38    pub fn id(&self) -> StreamId {
39        self.id
40    }
41
42    /// Runs `f` on this stream, restoring the previous stream afterward,
43    /// including on unwind.
44    pub fn enter<R>(&self, f: impl FnOnce() -> R) -> R {
45        self.id.executes(f)
46    }
47
48    /// Binds a future to this stream.
49    ///
50    /// The returned future re-establishes the stream around every poll, so the
51    /// binding survives executor work-stealing on any async runtime, without
52    /// requiring any particular executor.
53    pub fn attach<F: Future>(&self, fut: F) -> StreamFuture<F> {
54        StreamFuture {
55            id: self.id,
56            inner: fut,
57        }
58    }
59}
60
61/// A future bound to a [`Stream`], created with [`Stream::attach`].
62pub struct StreamFuture<F> {
63    id: StreamId,
64    inner: F,
65}
66
67impl<F: Future> Future for StreamFuture<F> {
68    type Output = F::Output;
69
70    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
71        // Manual pin projection: `inner` is structurally pinned, `id` is Copy.
72        // Safety: `inner` is never moved out of `self` after being pinned.
73        let (id, inner) = unsafe {
74            let this = self.get_unchecked_mut();
75            (this.id, Pin::new_unchecked(&mut this.inner))
76        };
77        id.executes(|| inner.poll(cx))
78    }
79}
80
81#[cfg(multi_threading)]
82impl Stream {
83    /// Runs `f` on a fresh OS thread bound to a fresh stream.
84    ///
85    /// Everything submitted inside `f` targets the new stream, concurrent with
86    /// work on other streams.
87    pub fn spawn<T, F>(f: F) -> StreamJoinHandle<T>
88    where
89        F: FnOnce() -> T + Send + 'static,
90        T: Send + 'static,
91    {
92        let stream = Self::new();
93        let id = stream.id;
94        let handle = std::thread::spawn(move || id.executes(f));
95
96        StreamJoinHandle { stream, handle }
97    }
98}
99
100/// Handle to a thread spawned with [`Stream::spawn`].
101#[cfg(multi_threading)]
102#[derive(Debug)]
103pub struct StreamJoinHandle<T> {
104    stream: Stream,
105    handle: std::thread::JoinHandle<T>,
106}
107
108#[cfg(multi_threading)]
109impl<T> StreamJoinHandle<T> {
110    /// The stream the spawned closure runs on.
111    pub fn stream(&self) -> Stream {
112        self.stream
113    }
114
115    /// Waits for the spawned closure to finish, returning its result.
116    pub fn join(self) -> std::thread::Result<T> {
117        self.handle.join()
118    }
119}
120
121#[cfg(tokio_rt)]
122impl Stream {
123    /// Spawns a future on the tokio runtime, bound to a fresh stream.
124    ///
125    /// Equivalent to `tokio::spawn(stream.attach(fut))`, returning the stream
126    /// so callers can relate results to it.
127    pub fn spawn_task<F>(fut: F) -> (Stream, tokio::task::JoinHandle<F::Output>)
128    where
129        F: Future + Send + 'static,
130        F::Output: Send + 'static,
131    {
132        let stream = Self::new();
133        (stream, tokio::spawn(stream.attach(fut)))
134    }
135}
136
137/// Spawns a detached future bound to a fresh stream.
138///
139/// Uses a thread on native, the browser runtime on wasm; panics on no-std.
140pub fn spawn_detached(fut: impl Future<Output = ()> + Send + 'static) -> Stream {
141    let stream = Stream::new();
142    crate::future::spawn_detached(stream.attach(fut));
143    stream
144}
145
146#[cfg(all(test, multi_threading))]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn enter_pins_the_stream() {
152        let stream = Stream::new();
153        let current = stream.enter(StreamId::current);
154        assert_eq!(current, stream.id());
155    }
156
157    #[test]
158    fn spawn_runs_on_its_own_stream() {
159        let handle = Stream::spawn(StreamId::current);
160        let expected = handle.stream().id();
161        assert_eq!(handle.join().unwrap(), expected);
162    }
163}
164
165#[cfg(all(test, tokio_rt))]
166mod tests_tokio {
167    use super::*;
168    use crate::stream::StreamPolicy;
169    use alloc::vec::Vec;
170
171    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
172    async fn attach_keeps_stream_across_awaits() {
173        let stream = Stream::new();
174        let id = stream.id();
175
176        let checks = stream.attach(async move {
177            for _ in 0..32 {
178                assert_eq!(StreamId::current(), id);
179                tokio::task::yield_now().await;
180            }
181        });
182
183        tokio::spawn(checks).await.unwrap();
184    }
185
186    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
187    // The guard serializes this test against the other policy-mutating ones, so
188    // it has to span the awaits: dropping it earlier is exactly the race it
189    // exists to prevent. Nothing awaited here takes that lock, so it can't
190    // deadlock.
191    #[allow(clippy::await_holding_lock)]
192    async fn per_task_ids_are_stable_and_distinct() {
193        let _guard = crate::stream::tests_policy_lock();
194
195        crate::stream::set_policy(StreamPolicy::PerTask);
196
197        let mut handles = Vec::new();
198        for _ in 0..8 {
199            handles.push(tokio::spawn(async {
200                let first = StreamId::current();
201                for _ in 0..32 {
202                    tokio::task::yield_now().await;
203                    assert_eq!(StreamId::current(), first);
204                }
205                first
206            }));
207        }
208
209        let mut ids = Vec::new();
210        for handle in handles {
211            ids.push(handle.await.unwrap());
212        }
213        ids.sort();
214        ids.dedup();
215        assert_eq!(ids.len(), 8, "each task should get its own stream id");
216
217        crate::stream::tests_reset_policy();
218    }
219}