diff --git a/09_pinning.md b/09_pinning.md index 1f00212ad80b591c687a1f5248adccb8e3993baa..fad4a2fd9bf0b056b51dc7d5fef669766ab05add 100644 --- a/09_pinning.md +++ b/09_pinning.md @@ -93,6 +93,20 @@ coroutine fn async_main() { ## Le compteur 1/ +```rust +impl Coroutine { + fn new() -> Self { + Self { + stack: Stack { counter: None }, + state: State::Start, + } + } +} + +``` + +## Le compteur 2/ + ```rust State::Start => { self.stack.counter = Some(0); @@ -119,7 +133,7 @@ State::Wait2(ref mut f2) => { } ``` -## Compteur 2/ +## Compteur 3/ * `Start`: on initialise le compteur * `Wait1`: @@ -132,3 +146,323 @@ State::Wait2(ref mut f2) => { * on affiche sa velur à l'écran * on "nettoie" la valeur (rien besoin de faire, car on a déjà `None` dans la pile) +# Partie 2: Ajouter des références + +## Objectif: ajouter un buffer dans notre code `async` + +```rust +coroutine fn async_main() { + let mut buffer = String::from("\nBUFFER:\n------\n"); + let writer = &mut buffer; + let mut counter = 0; + println!("Program starting"); + let txt = Http::get("/600/HelloAsyncWait").wait; + writeln!(writer, "{txt}").unwrap(); + counter += 1; + let txt = Http::get("/400/HelloAsyncWait").wait; + writeln!(writer, "{txt}").unwrap(); + writeln!(writer, "------").unwrap(); + counter += 1; + println!("Received {} responses", counter); + println!("{buffer}"); +} +``` + +* `buffer` sera une `Option<String>` +* `writer` est une `Option<*mut String>` +* Interdit de `Option<&mut String>`, car `writer` pointe sur `buffer` +* `writeln!()` permet d'écrire dans une autre cible que `println!()` + +## Le `buffer` 1/ + +```rust +struct Coroutine { + stack: Stack, + state: State, +} +struct Stack { + counter: Option<usize>, + buffer: Option<String>, + writer: Option<*mut String>, +} +impl Coroutine { + fn new() -> Self { + Self { + stack: Stack { counter: None, buffer: None, writer: None }, + state: State::Start, + } + } +} +``` + +* On étend notre `Stack` avec le `buffer` et `writer` + +## Le `buffer` 2/ + +### `Start` + +```rust +State::Start => { + self.stack.counter = Some(0); + self.stack.buffer = Some(String::from("\nBUFFER:\n------\n")); + self.stack.writer = Some(self.stack.buffer.as_mut().unwrap()); + println!("Program starting"); + let fut1 = Box::new(Http::get("/600/HelloAsyncWait")); + self.state = State::Wait1(fut1); +} +``` + +* On initialise `buffer/writer` +* On écrit dans le buffer via `writer` +* L'état est implicitement sauvé dans la struct `stack` +* `buffer.as_mut()` coercé en `*mut String` + +### `Wait1` + +```rust +State::Wait1(ref mut f1) => { + match f1.poll(waker) { + PollState::Ready(txt) => { + let mut counter = self.stack.counter.take().unwrap(); + let writer = unsafe { &mut *self.stack.writer.take().unwrap() }; + writeln!(writer, "{txt}").unwrap(); + counter += 1; + let fut2 = Box::new(Http::get("/400/HelloAsyncWait")); + self.state = State::Wait2(fut2); + self.stack.counter = Some(counter); + self.stack.writer = Some(writer); + } + PollState::NotReady => break PollState::NotReady, + } +} +``` + +* On prend l'ownership du `writer`, et on écrit dedans +* On remet le `writer` dans la `stack` +* On est obligé de faire du `unsafe` + +### `Wait2` + +```rust +State::Wait2(ref mut f2) => { + match f2.poll(waker) { + PollState::Ready(txt) => { + let mut counter = self.stack.counter.take().unwrap(); + let buffer = self.stack.buffer.as_ref().take().unwrap(); + let writer = unsafe { &mut *self.stack.writer.take().unwrap() }; + writeln!(writer, "{txt}").unwrap(); + writeln!(writer, "------").unwrap(); + counter += 1; + println!("Received {counter} responses"); + println!("{buffer}"); + self.state = State::Resolved; + let _ = self.stack.buffer.take(); + break PollState::Ready(String::new()); + } + PollState::NotReady => break PollState::NotReady, + } +} +``` + +* On prend une référence vers le `buffer` +* On **peut pas** prendre l'ownership du `buffer` => le `writer` serait invalidé +* On **prend l'ownership** de la référence vers le buffer +* On écrit dans le `writer` et affiche le `buffer` +* On `drop()` le bufffer à la fin pour libérer les ressources et éviter de garder les choses inutiles en mémoire + +# Apparté: optimisation + +## Optimisation + +* Pour éviter de `poll()` trop souvent, on pourrait optimiser un tout petit peu le code +* On modifie `block_on()` + + ```rust + pub fn block_on<F>(&mut self, future: F) + where + F: Future<Output = String> + 'static, + { + let waker = self.get_waker(usize::MAX); + let mut future = future; + match future.poll(&waker) { + PollState::NotReady => (), + PollState::Ready(_) => return, + } + spawn(future); + loop { + while let Some(id) = self.pop_ready() { + ... + ``` +* Si les `Future` parent est déjà `Ready` on a rien besoin de faire. + +## Le drame + +```console +Program starting +First poll - start operation +Data not ready +Data not ready +main: 1, pending tasks. Sleep until notified. +First poll - start operation +Data not ready +main: 1, pending tasks. Sleep until notified. +Received 2 responses +/400/HelloAsyncW +free(): double free detected in tcache 2 +``` + +* Hum, hum, `free(): double free detected in tcache 2` + +## Ok.... Il s'est passé quoi? + +1. On a un `Future` en argument de `block_on()` et tout va bien +2. On appelle `poll()` une première fois + * le `Future` est sur la pile de `block_on()` + * on appelle `poll()` et on est dans l'état `Start` + * initialisation de la structure auto-référencée qui fait pointer le `writer` sur le `buffer` qui est sur la pile +3. On retourne `NotReady` et le future est placé dans la `HashMap<usize, Box<dyn Future>>` de `Executor` +4. Le `Future` est désormais sur la pile +5. A l'appel suivant de `poll()` on essaie de lire le `buffer` via `writer` qui contient encore l'ancienne adresse... + +# Partie 4: Le Pinning + +## Le Pinning + +* Trouver un moyen de **fixer** la mémoire pour avoir la **garantie** de pourvoir partager un état +* Aucune garantie de l'OS que des données restent au même endroit +* En Rust: un type `Pin<T>` et un trait `Unpin` +* Un trait `Unpin` implémenté pour un type fait que `Pin<T>` n'a aucun effet +* `Pin<T>` un type qui est pas `Unpin` darantit qu'il bouge pas en mémoire +* `Pin<T>` est un *wrapper* autour des références et pointeurs intelligents +* Épingler un type qui n'est pas `Unpin` garantit que la valeur va rester à la même place en mémoire + +## Un exemple de Pinning 1/ + +```rust +struct MaybeSelfRef { + data: usize, + self_ref: Option<*mut usize>, + _pin: PhantomPinned, +} +``` + +* Des données, une auto-référence +* Un `PhantomPinned` qui indique au compilateur que rien ne devra bouger + +## Un exemple de Pinning 2/ + +```rust +impl MaybeSelfRef { + fn new() -> Self { + Self { + data: 0, + self_ref: None, + _pin: PhantomPinned::default(), + } + } +``` + +* On crée une struct poar "défaut" +* Pas encore auto-référencée + +## Un exemple de Pinning 3/ + +```rust +impl MaybeSelfRef { + fn init(self: Pin<&mut Self>) { + unsafe { + let Self { data, self_ref, .. } = self.get_unchecked_mut(); + *self_ref = Some(data); + } + } +} +``` + +* La structure devient auto-référencée que lorsqu'on appelle `init()` +* Le type en argument est `Pin<&mut Self>`: **Pin projection** + +## Un exemple de Pinning 4/ + +```rust +impl MaybeSelfRef { + fn self_ref(self: Pin<&mut Self>) -> Option<&mut usize> { + unsafe { self.get_unchecked_mut().self_ref.map(|sr| &mut *sr) } + } +} +``` + +* On cache le pointeur et le `unsafe` dans une fonction +* Transformer un pointeur en référence est intrisèquement `unsafe` car on sait pas si le pointeur est valide + +## Le Pinning sur le tas + +```rust +fn main() { + let mut x = Box::pin(MaybeSelfRef::new()); + x.as_mut().init(); + println!("Heap Pinning data {}", x.as_ref().data); + *x.as_mut().self_ref().unwrap() = 10; + println!("Heap Pinning modified {}", x.as_ref().data); +} +``` + +* On crée un `Box::pin()` (pointeur de données sur le tas) +* On initialise notre instance +* On modifie l'instance avec la `self_ref()` +* C'est "simple" d'épingler sur le tas (on peut mettre la valeur n'importe où) + +## Le Pinning sur la pile 1/ + +```rust +fn main() { + let mut x = MaybeSelfRef::new(); + let mut x = unsafe { Pin::new_unchecked(&mut x) }; + x.as_mut().init(); + println!(" Pinning data {}", x.as_ref().data); + *x.as_mut().self_ref().unwrap() = 10; + println!("Heap Pinning modified {}", x.as_ref().data); +} +``` + +* Presque pareil.... Sauf que c'est `unsafe` de créer l'instance épinglée `x` +* C'est difficile de garantir la position de `x` dans la pile parce que c'est structuré (faut une "bonne place") +* Si on fait cela à l'intérieur d'une fonction, on peut pas garantir que les données seront là (on drop tout à la sortie) +* C'est `unsafe` car les garanties de Rust peuvent pas être satisfaites + +## Le Pinning sur la pile 2/ + +```rust +fn main() { + println!("Problem with Stack pinning"); + let mut x = MaybeSelfRef::new(); + let mut y = MaybeSelfRef::new(); + { + unsafe { + let mut x = Pin::new_unchecked(&mut x); + x.as_mut().init(); + *x.as_mut().self_ref().unwrap() = 10; + }; + } + swap(&mut x, &mut y); +} +``` + +* On cache l'épinglage de `x` dans un bloc séparé. +* Quand on quitte `{}` on peut échanger les références en `safe` +* Ici `y` contiendra l'adresse de `x.data` et non celle de `y.data` + +## Le Pinning sur la pile 3/ + +```rust +fn main() { + let mut x = pin!(MaybeSelfRef::new()); + MaybeSelfRef::init(x.as_mut()); + println!("{}", x.as_ref().data); + *x.as_mut().self_ref().unwrap() = 2; + println!("{}", x.as_ref().data); +} +``` + +* La macro `pin!()` nous sauve +* Si on essaie de refaire l'exemple précent, on a une erreur de compilation +* Si on veut absolument faire un `Pin` sur la pile, utiliser `pin!()`