1. Escenario y restricciones
Usted mantiene una aplicación de edición de imágenes. La interfaz de usuario accede a una caché, mientras que las descargas de red, la decodificación y la generación de miniaturas son costosas. El código antiguo depende de saltos de hilo implícitos, algunas API son nonisolated async y las pruebas solo verifican la imagen final. Debe adoptar Swift 6.2, preservar un desplazamiento fluido y resultados deterministas, y habilitar progresivamente la comprobación de carreras de datos de Swift 6.
Comience registrando líneas base: tiempo de fotograma en el hilo principal, rendimiento de decodificación, recuento de tareas activas, tasa de aciertos de caché, latencia de cancelación y diagnósticos del compilador antes y después de la migración. Sin esto, es fácil confundir una regresión de rendimiento con una corrección de corrección.
2. Explicar primero los cambios de Swift 6.2
Swift 6.2 ofrece la configuración opcional -default-isolation MainActor, que aísla el código de un objetivo al actor principal por defecto. También agrega @concurrent para marcar funciones destinadas a ejecutarse de forma concurrente. Con NonisolatedNonsendingByDefault, una función nonisolated async conserva el actor del llamador por defecto en lugar de trasladarse automáticamente al ejecutor concurrente global; use @concurrent cuando se deseen semánticas paralelas.
Estas opciones reducen el ruido de anotaciones pero no hacen que cada operación sea paralela. El aislamiento por defecto describe quién puede acceder al estado de forma segura. @concurrent indica explícitamente que el trabajo puede abandonar el actor actual y ejecutarse en paralelo.
3. Establecer límites de aislamiento
Mantenga la caché mutable y el estado de la interfaz de usuario en un tipo @MainActor. Haga que las transformaciones de valores puros y los decodificadores con entradas y salidas inmutables sean Sendable. La capa de descarga debe devolver valores en lugar de objetos con afinidad de hilo oculta. Para cada API, documente el actor del llamador, el estado protegido y si se permite la ejecución paralela.
Por ejemplo, mantenga la protección de la caché en el actor principal:
@MainActor
final class ImageCache {
private var values: [URL: Image] = [:]
func value(for url: URL) -> Image? { values[url] }
func insert(_ image: Image, for url: URL) { values[url] = image }
}No disperse MainActor.run por el código antiguo solo para silenciar advertencias. La guía de migración lo trata como una herramienta de transición; las API a largo plazo deben expresar el aislamiento en sus tipos y firmas.
4. Decidir cuándo usar @concurrent
Agregue @concurrent solo cuando sea seguro ejecutar el trabajo en paralelo, sus entradas y salidas cumplan los requisitos de envío (sending requirements) y una línea base demuestre un beneficio. La decodificación de imágenes suele calificar: no toca directamente la caché del actor principal y devuelve un valor recién creado.
@MainActor
struct ImagePipeline {
static func image(from url: URL) async throws -> Image {
let cached = cache.value(for: url)
if let cached { return cached }
let image = try await decode(url: url)
cache.insert(image, for: url)
return image
}
@concurrent
private static func decode(url: URL) async throws -> Image {
let (data, _) = try await URLSession.shared.data(from: url)
return try ImageDecoder.decode(data)
}
}Señale que @concurrent no es un interruptor de velocidad. El uso excesivo añade costos de programación, memoria y sincronización; si el decodificador es internamente serial o las imágenes son diminutas, el rendimiento puede caer.
5. Manejar el contexto del llamador y Sendable
Con las semánticas de contexto del llamador habilitadas, una función async puede continuar en el actor del llamador. Eso ayuda cuando necesita un estado aislado al llamador, pero el trabajo prolongado de CPU puede bloquear ese actor, por lo que debe separar una etapa puramente @concurrent. Los valores que cruzan actores deben ser Sendable o estar protegidos por un actor, un bloqueo (lock) o un límite de sincronización equivalente.
No silencie cada diagnóstico con @unchecked Sendable. Úselo únicamente como un último recurso puntual tras demostrar invariantes, primitivas de sincronización y tiempo de vida, y acompáñelo de pruebas de estrés de concurrencia.
6. Migrar por etapas con herramientas
Habilite primero las características futuras y los diagnósticos estrictos en un módulo, utilizando herramientas de migración para advertencias y correcciones automáticas (fix-its). Estabilice las restricciones de aislamiento y Sendable en las API públicas, luego cambie ese módulo al modo de lenguaje Swift 6. Mantenga cada fase reversible y archive los diagnósticos por módulo.
Un orden práctico son los tipos de valor y las restricciones de protocolo, los servicios de bajo nivel, el actor de caché y finalmente los puntos de llamada en la interfaz de usuario. Así, los errores aparecen en los límites en lugar de en cientos de expresiones await modificadas. Durante el trabajo de compatibilidad, conserve las interfaces en modo Swift 5 para los clientes existentes mientras aumenta los niveles de comprobación dentro de los módulos de implementación.
7. Demostrar la ausencia de carreras y serialización accidental
Más allá de las pruebas funcionales, añada estrés de concurrencia: solicitudes simultáneas para una misma URL, cancelación aleatoria, escrituras repetidas y lecturas entre actores. Use Thread Sanitizer y diagnósticos estrictos de concurrencia para detectar carreras. Evalúe con benchmarks cachés frías y calientes, tamaños de imagen y límites de concurrencia, comparando tiempo de fotograma, rendimiento, memoria máxima y latencia de cancelación.
Inspeccione el contexto de tareas y las pilas asíncronas para confirmar que la decodificación realmente se ejecuta de forma concurrente mientras el acceso a la caché permanece en el actor principal. Si @concurrent no mejora el tiempo de fotograma, verifique si hay un decodificador bloqueante, un único bloqueo que serializa el trabajo o demasiadas tareas diminutas.
8. Rúbrica y preguntas de seguimiento
Debe explicar
- Distinguir el aislamiento por defecto, el contexto del llamador y el paralelismo explícito;
asyncno significa un hilo en segundo plano. - Demostrar la seguridad con
Sendable, límites de actores y cancelación en lugar de ocultar errores con@unchecked Sendable. - Proporcionar una migración por etapas, pruebas de estrés y líneas base de rendimiento con una explicación de las señales de regresión.
Preguntas de seguimiento
- Si el decodificador no es seguro para subprocesos (thread-safe), ¿lo ubicaría detrás de un actor, un ejecutor serial o un servicio fuera de proceso, y por qué?
- ¿Cómo combinaría descargas duplicadas para una misma URL sin serializar toda la caché?
- ¿Cómo pueden los módulos de Swift 5 y Swift 6 compartir API durante la migración y qué restricciones deben implementarse primero?
Guía de evaluación
Una respuesta excelente conecta el modelado de aislamiento, las herramientas de migración y la evidencia en tiempo de ejecución: define el estado y el contexto de ejecución, utiliza la superficie mínima necesaria de @concurrent para el paralelismo y luego valida el resultado con diagnósticos del compilador, Sanitizer y datos de benchmark.