Concurrencia en Swift
En cualquier programa escrito es Swift al iniciarse se crea un hilo de ejecución principal "@main". Este hilo de ejecución realiza cíclicamente un sondeo para detectar eventos de entradas, ejecutar código del usuario y actualizar la interfaz gráfica. Este sondeo es lo suficientemente rápido para dar la impresión que la aplicación reacciona al instante. Para lograr ello, el código síncrono, es decir el lineal, debe ser lo suficientemente optimizado. Pero esta capacidad de respuesta inmediata se ve afectada si se intenta ejecutar una operación pesada o que dependa de factores externos como velocidad y acceso a servicios en redes. Entonces se experimenta lo que conocemos como laps o retardos e incluso puede llegar a bloquearse por completo la aplicación.
La solución a este problema es el código concurrente. Debido a que Swift ejecuta el código asíncrono o concurrente fuera del hilo principal y pararelo a este, el hilo principal puede seguir ejecutándose y responder a los eventos del usuario, e incluso puede seguir ejecutando otro código sobre demanda.
Algo importante a tener en cuenta es que el código concurrente solo puede ser llamado desde otro código concurrente. Esta restricción esta implentada intencionalmente en Swift debido a que resulta muy inseguro llamar código concurrente desde código síncrono.
Una función o método asíncrono llava la palabra reservada async al final de la lista de parámetros y antes del valor de retorno, o de throws si es una función de lanzamiento. Ejemplo:
func listPhotos() async ->[Image]{
}
Para llamar a una función asincrónica debe utilizar la palabra reservada await que indica una posible pausa en la ejecución del código actual hasta el retorno de una función o método asíncrono. Ejemplo:
let result = await listPhotos()
Para llamara a una función asíncrona desde SwiftUI utilice Task para crear un contexto asíncrono en donde poder llamar la función:
Button("Tap me"){
Task{
await listPhotos()
}
}
Otro ejemplo, un poco más complejo, lo tenemos aqui: Una función que acepta dos parámetros, un String y como segundo un closure asíncrono con el valor del primer parámetro. Debido a que la función retorna un closure asíncrono, el parámetro de closure debe ser asíncrono también.
func caller(value: String, action: @escaping (String)async ->()) async ->(){
return await action(value)
}
Para llamar a esta función desde SwiftUI u otra función tendríamos:
Button("Tap me"){
Task{
await caller(value: "Yorj") {str in
print(str)
}
}
}
Operaciones asincrónicas en paralelo:
Aunque las operaciones asíncronas se realizan en paralelo, cada operación debe esperar a terminar para dar paso a la siguiente. Esto produce un código secuencial que no es óptimo para realizar varias tareas a la vez.
Para lograr que el código asíncrono se ejecute de forma paralela al código circundante utilice la notación: async let = nombreFunciónAsincrónica:
async let result1 = test1()
async let result2 = test2()
async let result3= test3()
Después cuando quiera recuperar el valor de estas funciones escriba await delante de cada constante:
let resultTotal = await [result1, result2, result3]
De esta forma se logra que las funciones sincrónicas se ejecuten en paralelo, sin haber esperas entre ellas.
Actores:
Debido a que las funciones asíncronas están asialadas entre si, es seguro ejecutarlas al mismo tiempo. Sin embargo a veces es útil compartir información entre funciones asíncronas. La solución a ello son los actores. Un actor es un tipo por referencia, como las clases, que permite compartir información entre código concurrente. Los actores garantizan que su estado mutable (sus propiedades) sean accedidas y modificadas por un código concurrente a la vez. Esto resuelve los posibles conflictos de acceso superpuesto sobre una misma propiedad. Un ejemplo de actor seria:
actor temperaturas {
var arrayTemp : [Double] = []
var tempMax : Double
init(tempMax : Double){
self.tempMax = tempMax
}
func addValor(valor : Double){
arrayTemp.append(valor)
}
}
Para acceder a este actor debe primero crear una instancia del mismo. Luego para leer algunas de sus propiedades se debe utilizar la palabra reservada await para indicar un posible punto de suspención:
let actor = temperaturas(tempMax: 14)
print( await actor.tempMax)
Para modificar el estado mutable de un actor debe crear un método dentro del propio actor para ello. Por ejemplo:
extensión temperaturas {
func setTempMax(value : Double){
self.tempMax = value
}
}
Propiedades Calculadas Async
Las propiedades calculadas también pueden ser asíncronas. La única regla es que deben ser de solo lectura. Utilice await para acceder a su valor:
struct yy {
var contents: Int { get async {
45
}
}
}
let tt= await yy().contents