¿Qué es un Actor y para que se utilizan?
Libranner Santos
15 abril, 20234min de lectura
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: Intfunc 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 usandoawait
.
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.