Preguntas
¿Qué problema concreto resuelve el macro #isolation en la concurrencia de Swift?
#isolation es una ayuda sintáctica (como #file o #line) que produce una referencia al actor que aisla el código en cuestión o nil si el código es nonisolated.
Se usa en conjunto con el modificador de parámetro isolated que permite ejecutar el código de un método aislado en el actor marcado con isolated.
#isolation ayuda a crear código genérico que recibe un parámetro isolated y se le pasa por defecto una referencia al actor que invocó dicho código.
¿Por qué las funciones asíncronas normalmente "rompen" el aislamiento del actor llamador?
Generalmente las funciones asíncronas son non-isolated, provocando posibles puntos de suspensión innecesarios y limitando la capacidad del código de trabajar con datos no-Sendable dentro del contexto de un actor.
¿En qué situaciones sería útil heredar el aislamiento del actor en lugar de usar @MainActor directamente?
Usar directamente @MainActor implica asumir que el código invocador también se ejecuta en MainActor. Lo ideal es no asumir esto y, en su lugar, mejor heredar el contexto de aislamiento del invocador.
Según el artículo, ¿cómo funciona el macro #isolation como argumento por defecto en un parámetro isolated?
#isolation devuelve una referencia al actor invocador o nil si se invocó desde nonisolated
¿Por qué el compilador se queja cuando sequentialMap llama al closure transform sin usar #isolation?
El closure transform, en Swift 6.0, es de tipo nonisolated(nonsending) y el método sequentialMap originalmente es nonisolated.
Invocar sequentialMap desde @MainActor implica usar un await porque se va a salir del dominio MainActor.
El closure que se pasa a transform está aislado en MainActor y usa valores de ese dominio (porque la colección names se definió en MainActor y, por ende, los valores de tipo Element están en ese actor - Aunque particularmente en este ejemplo, String es Sendable). Además, el closure captura el contexto de @MainActor, y al ser nonisolated(nonsending) no puede cruzar hacia el dominio nonisolated de sequentialMap de forma segura.
Entonces, parte de MainActor, sale a nonisolated, pero trae contexto del dominio de MainActor que no puede cruzar de forma segura (por ser nonsending). Por esta razón se explota.
extension Collection where Element: Sendable {
nonisolated func sequentialMap<Result: Sendable>(
transform: (Element) async -> Result
) async -> [Result] {
var results: [Result] = []
for element in self {
results.append(await transform(element))
}
return results
}
}
struct Test {
func execute() {
Task { @MainActor in
let names = ["Antoine", "Maaike", "Sep", "Jip"]
let lowercaseNames = await names.sequentialMap { name in
await lowercaseWithSleep(input: name)
}
print(lowercaseNames)
}
}
@MainActor
func lowercaseWithSleep(input: String) async -> String {
input.lowercased()
}
}
En la solución, en lugar de que el método sequentialMap sea nonisolated, y que transform sea nonisolated(nonsending), se hereda el dominio del actor invocador con isolated y se pasa por defecto #isolation. Esto provoca que el closure tranform se ejecute en el dominio de aislamiento de isolated y resuelve el error.
extension Collection where Element: Sendable {
func sequentialMap<Result: Sendable>(
isolation: isolated (any Actor)? = #isolation,
transform: (Element) async -> Result
) async -> [Result] {
var results: [Result] = []
for element in self {
results.append(await transform(element))
}
return results
}
}
¿Qué cambio específico en la firma de la función resuelve el problema de las data races?
isolation: isolated (any Actor)? = #isolation
¿Cuáles son los tres beneficios de rendimiento y seguridad que menciona el artículo al usar herencia de aislamiento?
- Evita usar suspensiones innecesarias.
- Permite pasar valores no-
Sendable. - Evita detrimento en el rendimiento al cambiar de contextos.
Recite
Sin mirar el código, ¿puedes describir cómo quedaría la firma completa de sequentialMap usando #isolation?
Explica con tus propias palabras la diferencia entre una función aislada en un actor y una función non-isolated.
¿Por qué no era suficiente marcar sequentialMap directamente con @MainActor para resolver el problema del ejemplo?
Revisión
¿En qué tipos de proyectos o situaciones reales aplicarías el macro #isolation? ¿Por qué el artículo advierte que no es para todos los proyectos?
¿Cómo se relaciona la herencia de aislamiento de actores con el concepto de "cambio de contexto" (context switching) y el rendimiento de la app?
¿Qué pregunta o concepto del artículo quedó menos claro y cómo podrías investigarlo más?
Nota
Al hacer los experimentos del código en Xcode 26.4 pude notar que los errores descritos por el artículo no salían. Esto se debe a que tenía marcada la opción por defecto SWIFT_APPROACHABLE_CONCURRENCY = YES que habilita la funcionalidad NonisolatedNonsendingByDefault de Swift 6.2 que cambia cómo funcionan los closures nonisolated async:
- En Swift 6.0 el closure
transformesnonisolatedySendable, lo que le permite cruzar hacia el contextononisolateddesequentialMapllevando valores del@MainActor— de ahí la carrera de datos y el error del compilador. - En Swift 6.2, el closure transform se vuelve implícitamente
nonisolated(nonsending), lo que impide que cruce fronteras de concurrencia de forma insegura. Por eso el compilador ya no reporta el error. - El macro
#isolationera la solución manual a este problema en Swift 6.0, que Swift 6.2 resuelve automáticamente conNonisolatedNonsendingByDefault.
Según el razonamiento anterior, podría pensar que marcar con nonisolated(nonsending) al closure transform bastaría para que no salga el error, sin embargo, esto no es así.
nonisolated(nonsending) le dice al compilador: "este closure no puede cruzar fronteras", pero sequentialMap sigue siendo nonisolated. Por esta razón, el compilador detecta que estoy mandando el closure a otro dominio y reporta el error. Por esta razón, es necesario poner el parámetro isolation: isolated (any Actor)? = #isolation, lo que le permite al compilador deducir el actor que aisla al método sequentialMap.
Cuando Swift 6.2 aplica nonisolated(nonsending) automáticamente con SWIFT_APPROACHABLE_CONCURRENCY = YES también ajusta cómo el compilador razona sobre el sitio de llamada:
- El compilador ya sabe que el closure es
nonisolated(nonsending)y, en lugar de tratarlo como un valor que se envía asequentialMap, lo trata como un valor que permanece en el dominio del llamador (@MainActor) durante toda la ejecución. Así se cumple la restricción del closure sin necesidad de anclar la función con#isolation.
Top comments (0)