¿Qué es un Actor y para que se utilizan?

Cuando utilizamos código asíncrono recurrimos a la concurrencia para aprovechar la capacidad de procesamiento ejecutando código en diferentes hilos. Esto puede provocar condiciones de carrera o race conditions en inglés, que se refiere a cuando dos o más hilos intentan accesar/modificar valores al mismo tiempo. Podemos evitar estos problemas creando objetos de tipo actor.

Un actor es un tipo que provee sincronización para estado compartido que no es constante, es decir, que puede ser modificado en tiempo de ejecución.

¿Por qué es importante evitar las condiciones de carrera? Evitar las condiciones de carrera es una de las principales utilidades de los actores. Las características de estas son las siguientes:

  • No determinísticas, las funciones no siempre producirán el mismo resultado.
  • Son causadas por estados mutables que son compartidos por múltiples objetos.
  • Dos o más hilos intentan acceder la misma data, y al menos uno esta intentando modificar dicha data.

Características de un Actor

A continuación veremos las características de un actor, para esto tomemos el siguiente ejemplo:

actor VisitsCounter {
var numberOfVisits: Int
func addNewVisit() {
numberOfVisits += 1
}
}

En términos de sintaxis un actor no se diferencia demasiado de una estructura o una clase, en este caso, si no fuese por la palabra reservada actor serían identicos.

Para llamar el método addNewVisit() desde otro fuera del actor en nuestro código o para acceder a numberOfVisits debemos usar await. Por ejemplo:

let counter = VisitsCounter()
Task {
await counter.addNewVisit()
}

Esto permite a los actores coordinar el acceso al estado y evitar las condiciones de carrera, ya que al usar await agregáramos nuestra petición al objeto a una cola, y el actor decidirá cuando responder al pedido. Las llamadas son suspendidas hasta que sean resumidas por el actor para dar respuesta.

Por otro lado, las llamadas dentro de un actor son siempre síncronas, por lo que no tenemos que usar await. Por eso en nuestro actor cambiamos el valor de numberOfVisits sin usar await.

Otras características de un actor a tener en cuenta

  • Tienen su propio estado, el cual está aislado del resto del programa.
  • Todo acceso al estado es realizado a través del actor. De este modo, aseguramos que el acceso al estado es mutualmente excluido, es decir, que nunca dos hilos puedes acceder al mismo tiempo a este.
  • Son de tipo referencia, al igual que las clases.
  • No soportan herencia.
  • El estado de un actor puede cambiar durante una suspensión, es decir, mientras esperamos que un código precedido por await se ejecute. Se recomienda verificar que el estado se encuentra como esperabas, luego de ejecutar un código usando await.

Swift nos permite usar un actor para garantizar que el código se ejecuta en el hilo principal, Main Actor.

Usar nonisolated para permitir ejecutar código síncrono

Podemos usar el modificador nonisolated para indicar que partes del código de un actor deben ser accedidas como si estuviesen fuera del actor. Dicho de otra manera, debemos usar nonisolated cuando es obligatorio que un método se ejecute de manera síncrona, pero esta implementado dentro del actor.

Es importante saber que métodos marcados con nonisolated no pueden acceder variables mutables dentro del actor.

Imaginemos que necesitamos hacer que nuestro actor conforme al protocolo Hashable. Tendríamos que usar el siguiente código:

extension VisitsCounter: Hashable {
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(numberOfVisits)
}
}

Hashable necesita ejecutarse de manera síncrona, por lo que si no usamos nonisolated el compilador nos mostrará el siguiente error:

actor-isolated method hash(into:) cannot satisfy sinchronous requirement

Sendable

Si dentro de un actor necesitamos crear propiedades que sean de tipo clases, debemos asegurarnos que estas conforme a Sendable.

Sendable se refiere a tipos cuyas propiedades pueden ser compartidas de manera segura en código concurrente. Para esto todas las propiedades que conforman la clase (o estructura) tienen que conformar a Sendable.

Funciones y Closures Sendable

Para funciones y closures, usamos el atributo @Sendable, debemos tener en cuenta algunas restricciones:

  • No puede capturar una variable mutable local, porque esto podría producir condiciones de carrera.
  • Lo que sea que sea capturado por el closure debe conformar a Sendable.
  • Un closure síncrono nunca puede ser aislado, porque esto nos permitiría ejecutar código dentro del actor desde fuera.

Comparte este artículo

Subscríbete a nuestro Newsletter

Mantente al día en el mundo de las aplicaciones móviles con nuestro blog especializado.

Artículos semanales

Todas las semanas artículos nuevos sobre el mundo de las aplicaciones móviles.

No spam

No te enviaremos spam, solo contenido de calidad. Puedes darte de baja cuando quieras.

Contenido de calidad

Nada de contenido generado de manera automática usando ChatGPT.

Recomendaciones

Tips indispensables sobre mejores prácticas y metodologías.

© 2024 AsyncLearn