Muy buenas, me llamo Luis y aquí les traigo este nuevo tutorial.
Estaba leyendo sobre el PhantomData type y encontré el punto de que se puede utilizar como un mecanismo para controlar la vida útil. A partir de aquí comencé a jugar un poco con eso y se me ocurrió algo interesante.
El ejemplo en sí mismo puede ser un poco incómodo, pero claro conmigo 🙂
Índice
El escenario 🗺️
En el siguiente ejemplo, tenemos dos subprocesos, uno leyendo (Lector) de un archivo cada 100 milisegundos y uno escribiendo (Escritor) en un archivo con algunas pausas entre escrituras. El lector y el escritor son independientes y no se conocen entre sí.
Cuando se suelta el Reader, queremos detener nuestro hilo asociado con el Reader. Esto se hace en la implementación de Drop.
Necesita algunos cambios 🚧
Hay un problema con este código. Dado que el Reader se declara en el bloque interno, el Reader se eliminará incluso antes de que tenga la oportunidad de leer cualquiera de las escrituras en el archivo.
El problema es bastante fácil de solucionar en este ejemplo, simplemente eliminamos el bloque interno y todo está bien. Sin embargo, realmente no soluciona el problema en el panorama general. La base del código podría ser grande y compleja y un problema como este se vuelve no trivial.
Queremos asegurarnos de que el lector siempre lea la última escritura al menos una vez. En otras palabras, queremos que el lector viva tanto o más que el escritor.
Buscando una solución 🕵️♀️
Entonces, ¿cómo podemos asegurarnos de que el escritor nunca sobreviva al lector? Una solución es hacer que Writer contenga una referencia al Reader como un campo en la estructura Writer. Esto funciona, pero nos obliga a aumentar el tamaño del Writer. También mancha un poco la semántica del escritor. En nuestro escenario inventado, no tiene sentido almacenar una referencia al lector.
¡Ingrese PhantomData 👻!
¿Qué está pasando aquí? 👩🔬
La versión modificada del código ahora tiene PhantomData en la mezcla. La estructura Writer ahora contiene un campo con el tipo PhantomData, más específicamente PhantomData <& ‘a ()>. Lo interesante aquí es que la parte importante es el parámetro de duración y no el parámetro de tipo, que es solo el tipo vacío “()” de Rust.
Al declarar el campo PhantomData de esta manera, la estructura Writer ahora declara que se puede hacer dependiente de alguna vida externa. Dado que no está relacionado con ningún tipo significativo, puede pensar en este campo como un mecanismo para controlar cuánto tiempo debe existir el escritor. No está vinculado a la estructura de Reader de ninguna manera.
La recompensa 🎖️
La segunda versión del ejemplo no se compila y eso es genial. El compilador se quejará de que Reader no vive lo suficiente, y eso es exactamente lo que queremos. El código declara el Reader en un bloque interno para simular una vida útil más corta para el Reader, en comparación con el Writer declarado.
Esto significa que cuando intentamos crear un nuevo Writer en el bloque interior, Rust se dará cuenta de que el Reader se eliminará al final del bloque interior, pero no el Writer. El escritor necesita sobrevivir al alcance interno y extender su vida útil hasta que finalice el bloqueo externo. Si elimina el bloque interno, el código se compilará y se ejecutará de la forma en que pretendemos que lo haga.
Lo bueno de esto es que, en lugar de dedicar tiempo a depurar en tiempo de ejecución posibles errores de hilo, puede encontrar algunos de los errores durante el tiempo de compilación.
Nota final 🗒️
Puede ser un poco complicado ver toda la «magia» en el código, ya que el compilador infiere la mayoría de las vidas. Te animo a que copies el código y juegues un poco con él, solo para ver qué sucede cuando cambias las cosas.
Gracias por leer.
Añadir comentario