La concurrencia avanzada de Swift va más allá de async/await y los actores básicos. Los custom executors permiten controlar exactamente en qué hilo corre un actor; los actores globales propios (@globalActor) crean dominios de aislamiento reutilizables; nonisolated declara que un método no necesita aislamiento; y assumeIsolated resuelve los casos donde el compilador no puede verificar el aislamiento pero el desarrollador sabe que es correcto.
Custom executors con SerialExecutor
Por defecto, los actores usan el cooperative thread pool de Swift. Un custom executor redirige la ejecución a un hilo específico, útil para integrar con APIs que exigen ejecutarse siempre en el mismo hilo (OpenGL, SQLite en modo WAL, librerías C con estado por hilo):
import Foundation
// 1. Crear el executor: un SerialDispatchQueueExecutor
final class HiloFijoExecutor: SerialExecutor {
private let queue: DispatchQueue
init(etiqueta: String) {
self.queue = DispatchQueue(label: etiqueta)
}
func enqueue(_ trabajo: consuming ExecutorJob) {
let trabajoCopy = UnownedJob(trabajo)
queue.async {
trabajoCopy.runSynchronously(on: self.asUnownedSerialExecutor())
}
}
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}
// 2. Actor que usa el executor personalizado
actor BaseDatosActor {
// El executor garantiza que todo el código del actor
// corre en esta queue específica
nonisolated var unownedExecutor: UnownedSerialExecutor {
ejecutor.asUnownedSerialExecutor()
}
private let ejecutor = HiloFijoExecutor(etiqueta: "com.miapp.bbdd")
private var conexion: OpaquePointer? // Conexión SQLite, por ejemplo
init() {
// La inicialización también corre en el executor
}
func consultar(_ sql: String) -> [[String: Any]] {
// Corre en el hilo de 'ejecutor', seguro para SQLite
[]
}
}
Definir un @globalActor propio
@MainActor es el actor global más conocido. Puedes crear los tuyos para aislar dominios de trabajo específicos (base de datos, rendering, lógica de audio):
// Definir el actor global
@globalActor
actor AudioActor {
static let shared = AudioActor()
private init() {}
}
// Anotar clases o métodos que pertenecen a este dominio
@AudioActor
class MotorAudio {
var volumen: Float = 1.0
func reproducir(archivo: URL) {
// Todo este código corre en AudioActor.shared
print("Reproduciendo: (archivo.lastPathComponent)")
}
}
// Función global aislada al AudioActor
@AudioActor
func ajustarVolumen(_ nivel: Float) {
MotorAudio().volumen = nivel
}
// Llamar desde código no aislado:
Task {
await ajustarVolumen(0.8) // await obligatorio fuera del actor
}
// Dentro de @AudioActor no hace falta await:
@AudioActor
func inicializarAudio() {
ajustarVolumen(1.0) // sin await: mismo dominio de aislamiento
}
nonisolated: métodos sin aislamiento
A veces un método de un actor no accede a estado mutable y puede ejecutarse desde cualquier contexto sin necesidad de await. nonisolated lo declara explícitamente:
actor GestorSesion {
var token: String?
let appID: String // inmutable, puede leerse sin aislamiento
init(appID: String) {
self.appID = appID
}
// nonisolated: no accede a estado mutable del actor
nonisolated func urlBase() -> URL {
URL(string: "https://api.ejemplo.com/(appID)")!
}
// nonisolated + Hashable/Equatable son necesarios en Swift 6
nonisolated var id: String { appID }
}
// urlBase() se puede llamar sin await desde cualquier contexto:
let gestor = GestorSesion(appID: "miapp")
let url = gestor.urlBase() // sin await
assumeIsolated: cuando el compilador no puede verificar
Hay situaciones donde el desarrollador sabe que el código corre en el contexto correcto pero el compilador no puede verificarlo por ejemplo, dentro de un callback de una librería C que el desarrollador llama siempre desde el hilo correcto:
@MainActor
class VistaController {
var estado: String = "inicial"
func configurarCallbackLibreriaC() {
// La librería C llama este closure en el hilo principal,
// pero el compilador no lo sabe
LibreriaC.setCallback {
// Sin assumeIsolated: error de compilación en Swift 6
// porque el closure no está aislado a @MainActor
MainActor.assumeIsolated {
// Dentro: el compilador confía en nuestra afirmación
self.estado = "actualizado" // acceso seguro
}
}
}
}
assumeIsolated lanza un error en runtime si la afirmación es falsa (el código no corre en ese actor), por lo que es más seguro que un cast sin verificación.
