Async/Await (Parte 11 de 11)
1Paralelización con async let
2Calcular tiempo de ejecución de un código
3¿Qué es un Task?
4¿Qué es un Actor y para que se utilizan?
5¿Qué es Main Actor?
6Modificador task
7Cómo utilizar Task Group
8Trabajando con iteraciones asíncronas usando AsyncSequence
9Mezclando código síncrono y asíncrono usando CheckedContinuation
10Optimiza la eficiencia de tu código concurrente con DiscardingTaskGroup
11Explorando Task-local
Explorando Task-local
Libranner Santos
14 agosto, 20234min de lectura
Con el nuevo modelo de concurrencia, el equipo detrás de Swift agrega más herramientas cada año para que podamos trabajar con código asíncrono de manera más estructurada. Este año, Apple introdujo Task-local, una nueva herramienta que nos ayuda a manejar almacenamiento que solo queremos que esté disponible dentro de un hilo.
Un Task-local es un dato asociado a una tarea específica, o más precisamente, a una jerarquía de tareas. Es como una variable global, pero el valor solo está disponible dentro de la jerarquía de tareas del contexto donde se ejecuta.
Ya existían APIs con comportamientos similares, como Task.currentPriority
y Task.isCancelled
, aunque Task-local no usa el mismo mecanismo de almacenaje. Esto nos muestra cómo este mismo patrón para acceder a metadatos en hilos está siendo usado en el nuevo paradigma de concurrencia moderna de Swift.
Características y limitantes de Task-local
Ahora veremos cómo podemos hacer uso de esta nueva herramienta y explicaremos sus ventajas y desventajas.
Crear una Task-local
Para crear un Task-local, debes anotar la variable usando el property wrapper @TaskLocal
y que sea static
. Ejemplo:
@TaskLocal static var myVariable: String = "No data"
Las variables que usen @TaskLocal
pueden leerse en cualquier función dentro del contexto donde fue creada, incluyendo procesos síncronos.
Cada declaración de un Task-local representa su propio e independiente espacio de almacenamiento.
Leer un Task-local
Para acceder a los valores, lo hacemos como cualquier otra propiedad estática. Ejemplo:
func execute() async {print(MyClass.myVariable)}
Asignar un valor al Task-local
Podemos asignar un valor usando TaskLocal.withValue(_:operation:file:line:)
, pero no podremos cambiarlo luego. Veamos un ejemplo:
await MyClass.$myVariable.withValue("Hello") {await execute()}await execute()
Es importante tener en cuenta que no puedes cambiar el valor de un Task-local luego de definido. En este código se imprimirá lo siguiente:
Como es código asíncrono podría variar el orden en que se imprimen los resultados.
HelloNo Data
Hello
es el valor que estamos asignando a MyClass.myVariable
en el contexto de la primera llamada a execute()
, sin embargo, como en la segunda llamada no estamos asignando ningún valor, se imprime No Data
, que es el valor por defecto.
El Task-local será retenido en memoria mientras exista el contexto donde fue ejecutado y todas las tareas hijas hayan terminado.
Cuando usar Task-local
Antes de determinar cuándo usarlo, es importante aclarar cuándo no. El objetivo de esta herramienta no es que la usemos para compartir estados globales de nuestras aplicaciones.
Aunque Task-local puede parecer útil, la recomendación es usarla solo si nuestro caso de uso no se resuelve simplemente pasando un parámetro. Acceder a un Task-local es más costoso que acceder a parámetros pasados explícitamente, además de que no tenemos ayuda en tiempo de compilación. Si olvidamos llamar a TaskLocal.withValue(_:operation:file:line:)
, podríamos introducir errores.
Task-local puede ser muy útil para instrumentación, como por ejemplo para crear un sistema para administrar logs.
Otras cosas a tomar en cuenta
- Los tipos de datos alojados en un Task-local deben cumplir con
Sendable
. Pueden ser de tipo valor o de tipo referencia. - Cuando usamos tareas estructuradas, como TaskGroup, los Task-local son heredados por todas las tareas hijas. Sin embargo, las
Task.detached
no heredan. En el caso deTask { ... }
, hereda los Task-local, ya que se hace una copia de los que se encontraban en el contexto al momento de su creación. Para más información sobreTask
yTask.detached
, puedes leer este artículo ¿Qué es un Task?. - El tiempo de vida de un Task-local está asociado al tiempo de vida de la tarea.
- Cuando leemos el valor de un Task-local, se recorre el contexto donde se encuentra la tarea hasta que se encuentre una tarea padre donde se haya definido. Si no se encuentra ninguna, se retorna el valor por defecto, que puede ser
nil
si así se indica en la definición, por ejemplo:
@TaskLocal static var myVariable: String?
Conclusión
A manera de resumen veamos el siguiente código donde utilizamos Task-local de diferentes maneras:
execute() // Imprime: No dataawait MyClass.$myVariable.withValue("Hello") {print(MyClass.myVariable) // Imprime `Hello`await MyLibrary.$requestID.withValue("Bye") {await execute() // Imprime `Bye`}await execute() // prints: `Hello`}print(MyClass.myVariable) // Imprime: No data
Como podemos ver nuestro programa imprimirá Hello
o Bye
dependiendo el contexto y sin importar si la llamada es síncrona o asíncrona. En los casos donde no se ha indicaco un valor para myVariable
, como el caso de la primera llamada a execute()
y el último print(MyClass.myVariable)
imprimirá No Data
.
Task-local es una herramienta útil para compartir metadata en jerarquía de tareas, siempre y cuando estemos al tanto de sus limitantes.
Async/Await (Parte 11 de 11)
1Paralelización con async let
2Calcular tiempo de ejecución de un código
3¿Qué es un Task?
4¿Qué es un Actor y para que se utilizan?
5¿Qué es Main Actor?
6Modificador task
7Cómo utilizar Task Group
8Trabajando con iteraciones asíncronas usando AsyncSequence
9Mezclando código síncrono y asíncrono usando CheckedContinuation
10Optimiza la eficiencia de tu código concurrente con DiscardingTaskGroup
11Explorando Task-local