-
Adopción de la concurrencia en Swift
Únete para aprender los conceptos básicos de la concurrencia en Swift. La concurrencia ayuda a mejorar la capacidad de respuesta y el rendimiento de la app, y Swift está diseñado para que sea más fácil escribir correctamente código asincrónico y concurrente. Mencionaremos los pasos que necesitas seguir para convertir una app de un solo subproceso a una concurrente. También te ayudaremos a determinar cómo y cuándo hacer el mejor uso de las características de concurrencia en Swift, ya sea haciendo que tu código sea más asincrónico, moviéndolo a un segundo plano o compartiendo datos entre tareas simultáneas.
Capítulos
- 0:00 - Introducción
- 3:17 - Código de un solo subproceso
- 6:00 - Tareas asincrónicas
- 7:24 - Entrelazado
- 10:22 - Introducción a la concurrencia
- 11:07 - Funciones concurrentes
- 13:10 - Código no aislado
- 14:13 - Grupo de subprocesos concurrentes
- 14:58 - Compartir datos
- 15:49 - Tipos de valores
- 17:16 - Tipos de actores aislados
- 18:30 - Clases
- 23:18 - Actores
- 26:12 - Conclusión
Recursos
Videos relacionados
WWDC25
WWDC23
-
Buscar este video…
-
-
3:20 - Single-threaded program
var greeting = "Hello, World!" func readArguments() { } func greet() { print(greeting) } readArguments() greet() -
4:13 - Data types in a the app
struct Image { } final class ImageModel { var imageCache: [URL: Image] = [:] } final class Library { static let shared: Library = Library() } -
4:57 - Load and display a local image
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) throws { let data = try Data(contentsOf: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } } final class Library { static let shared: Library = Library() } -
5:36 - Fetch and display an image over the network
import Foundation struct Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) throws { let (data, _) = try URLSession.shared.data(from: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } } final class Library { static let shared: Library = Library() } -
6:10 - Fetch and display image over the network asynchronously
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } } final class Library { static let shared: Library = Library() } -
7:31 - Creating a task to perform asynchronous work
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() var url: URL = URL("https://swift.org")! func onTapEvent() { Task { do { try await fetchAndDisplayImage(url: url) } catch let error { displayError(error) } } } func displayError(_ error: any Error) { } func fetchAndDisplayImage(url: URL) async throws { } } final class Library { static let shared: Library = Library() } -
9:15 - Ordered operations in a task
import Foundation class Image { func applyImageEffect() async { } } final class ImageModel { func displayImage(_ image: Image) { } func loadImage() async -> Image { Image() } func onButtonTap() { Task { let image = await loadImage() await image.applyImageEffect() displayImage(image) } } } -
9:38 - Fetch and display image over the network asynchronously
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } } -
10:40 - Fetch and display image over the network asynchronously
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = decodeImage(data, at: url) view.displayImage(image) } func decodeImage(_ data: Data, at url: URL) -> Image { Image() } } -
11:11 - Fetch over network asynchronously and decode concurrently
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { Image() } } -
11:30 - Implementation of decodeImage
final class View { func displayImage(_ image: Image) { } } final class ImageModel { var cachedImage: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { if let image = cachedImage[url] { return image } // decode image let image = Image() cachedImage[url] = image return image } } -
12:37 - Correct implementation of fetchAndDisplayImage with caching and concurrency
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var cachedImage: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { if let image = cachedImage[url] { view.displayImage(image) return } let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data) view.displayImage(image) } @concurrent func decodeImage(_ data: Data) async -> Image { // decode image Image() } } -
13:30 - JSONDecoder API should be non isolated
// Foundation import Foundation nonisolated public class JSONDecoder { public func decode<T: Decodable>(_ type: T.Type, from data: Data) -> T { fatalError("not implemented") } } -
15:18 - Fetch over network asynchronously and decode concurrently
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { Image() } } -
16:30 - Example of value types
// Value types are common in Swift import Foundation struct Post { var author: String var title: String var date: Date var categories: [String] } -
16:56 - Sendable value types
import Foundation // Value types are Sendable extension URL: Sendable {} // Collections of Sendable elements extension Array: Sendable where Element: Sendable {} // Structs and enums with Sendable storage struct ImageRequest: Sendable { var url: URL } // Main-actor types are implicitly Sendable @MainActor class ImageModel {} -
17:25 - Fetch over network asynchronously and decode concurrently
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await self.decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { Image() } } -
18:34 - MyImage class with reference semantics
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scale(by factor: Double) { } } let image = MyImage() let otherImage = image // refers to the same object as 'image' image.scale(by: 0.5) // also changes otherImage! -
19:19 - Concurrently scaling while displaying an image is a data race
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start func scaleAndDisplay(imageName: String) { let image = loadImage(imageName) Task { @concurrent in image.scaleImage(by: 0.5) } view.displayImage(image) } // Slide content end func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } } -
20:38 - Scaling and then displaying an image eliminates the data race
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() func scaleAndDisplay(imageName: String) { Task { @concurrent in let image = loadImage(imageName) image.scaleImage(by: 0.5) await view.displayImage(image) } } nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } } -
20:54 - Scaling and then displaying an image within a concurrent asynchronous function
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() @concurrent func scaleAndDisplay(imageName: String) async { let image = loadImage(imageName) image.scaleImage(by: 0.5) await view.displayImage(image) } nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } } -
21:11 - Scaling, then displaying and concurrently modifying an image is a data race
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } func applyAnotherEffect() { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start @concurrent func scaleAndDisplay(imageName: String) async { let image = loadImage(imageName) image.scaleImage(by: 0.5) await view.displayImage(image) image.applyAnotherEffect() } // Slide content end nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } } -
21:20 - Applying image transforms before sending to the main actor
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } func applyAnotherEffect() { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start @concurrent func scaleAndDisplay(imageName: String) async { let image = loadImage(imageName) image.scaleImage(by: 0.5) image.applyAnotherEffect() await view.displayImage(image) } // Slide content end nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } } -
22:06 - Closures create shared state
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scale(by factor: Double) { } func applyAnotherEffect() { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start @concurrent func scaleAndDisplay(imageName: String) async throws { let image = loadImage(imageName) try await perform(afterDelay: 0.1) { image.scale(by: 0.5) } await view.displayImage(image) } nonisolated func perform(afterDelay delay: Double, body: () -> Void) async throws { try await Task.sleep(for: .seconds(delay)) body() } // Slide content end nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }pet. -
23:47 - Network manager class
import Foundation nonisolated class MyImage { } struct Connection { func data(from url: URL) async throws -> Data { Data() } } final class NetworkManager { var openConnections: [URL: Connection] = [:] func openConnection(for url: URL) async -> Connection { if let connection = openConnections[url] { return connection } let connection = Connection() openConnections[url] = connection return connection } func closeConnection(_ connection: Connection, for url: URL) async { openConnections.removeValue(forKey: url) } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() let networkManager: NetworkManager = NetworkManager() func fetchAndDisplayImage(url: URL) async throws { if let image = cachedImage[url] { view.displayImage(image) return } let connection = await networkManager.openConnection(for: url) let data = try await connection.data(from: url) await networkManager.closeConnection(connection, for: url) let image = await decodeImage(data) view.displayImage(image) } @concurrent func decodeImage(_ data: Data) async -> MyImage { // decode image return MyImage() } } -
25:10 - Network manager as an actor
import Foundation nonisolated class MyImage { } struct Connection { func data(from url: URL) async throws -> Data { Data() } } actor NetworkManager { var openConnections: [URL: Connection] = [:] func openConnection(for url: URL) async -> Connection { if let connection = openConnections[url] { return connection } let connection = Connection() openConnections[url] = connection return connection } func closeConnection(_ connection: Connection, for url: URL) async { openConnections.removeValue(forKey: url) } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() let networkManager: NetworkManager = NetworkManager() func fetchAndDisplayImage(url: URL) async throws { if let image = cachedImage[url] { view.displayImage(image) return } let connection = await networkManager.openConnection(for: url) let data = try await connection.data(from: url) await networkManager.closeConnection(connection, for: url) let image = await decodeImage(data) view.displayImage(image) } @concurrent func decodeImage(_ data: Data) async -> MyImage { // decode image return MyImage() } }
-
-
- 0:00 - Introducción
La concurrencia de Swift permite que las apps realicen tareas simultáneamente, lo que mejora la capacidad de respuesta y descarga el procesamiento en segundo plano. El modelo de concurrencia de Swift facilita escribir código concurrente de forma correcta al hacer explícita la introducción de la concurrencia, identificar los datos compartidos entre tareas concurrentes y detectar posibles carreras de datos en tiempo de compilación. Las apps comienzan ejecutando todo el código en el hilo principal. Al aumentar la complejidad, se pueden presentar tareas asincrónicas para operaciones de alta latencia, como el acceso a la red. Los hilos en segundo plano se pueden usar para tareas que requieren un mayor esfuerzo computacional. Swift brinda herramientas como actores y tareas para expresar estas operaciones concurrentes.
- 3:17 - Código de un solo subproceso
En Swift, el código de un solo hilo se ejecuta en el hilo principal, que está aislado del actor principal. No hay concurrencia en el actor principal porque solo hay un hilo principal que puede ejecutarlo. Se puede especificar que los datos o el código están en el actor principal con la notación @MainActor. Swift garantizará que el código del actor principal solo se ejecute en el hilo principal y que a los datos del actor principal solo se acceda desde allí. Decimos que dicho código está aislado del actor principal. Swift protege el código del hilo principal mediante el actor principal, lo que garantiza que se pueda acceder libremente al estado compartido. La protección del código con el actor principal depende de la configuración de compilación predeterminada. Usa esto principalmente para el módulo de la app principal y cualquiera que se centre en las interacciones de la IU.
- 6:00 - Tareas asincrónicas
La concurrencia en Swift usa async y await para que las funcionalidades no bloqueen la ejecución, permitiendo que otras tareas se realicen mientras se espera por datos, como solicitudes de red. Esto evita bloqueos y mejora la capacidad de respuesta de la IU al dividir las funcionalidades en partes que se ejecutan antes y después del evento esperado.
- 7:24 - Entrelazado
Las funcionalidades asincrónicas se ejecutan en una tarea independientemente de otras tareas. Un solo hilo puede alternar entre tareas independientes a medida que estén listas, mediante “interleaving.” Esto mejora el rendimiento al evitar tiempos de inactividad y hace un uso eficiente de los recursos del sistema. Las tareas asincrónicas múltiples son efectivas cuando se realizan muchas operaciones independientes al mismo tiempo. Al realizar un trabajo en un orden específico, usa una sola tarea. El uso de tareas asincrónicas en un solo hilo suele ser suficiente. Si el hilo principal se sobrecarga, herramientas de análisis como Instruments pueden identificar cuellos de botella para optimizar el rendimiento antes de introducir concurrencia.
- 10:22 - Introducción a la concurrencia
La concurrencia permite que partes del código se ejecuten en un hilo en segundo plano en paralelo con el hilo principal, lo que acelera el trabajo al aprovechar mejor los núcleos del CPU del sistema. Para mejorar el rendimiento, el ejemplo presenta concurrencia para ejecutar código en hilos en segundo plano, liberando el hilo principal.
- 11:07 - Funciones concurrentes
Al aplicar el atributo @concurrent, se le indica a Swift que ejecute una funcionalidad en segundo plano. El compilador Swift resalta el acceso a los datos en el actor principal para presentar concurrencia de forma segura. Una práctica recomendada para garantizar que el trabajo se realice de manera sincrónica es mover el código del actor principal a un llamador que siempre se ejecute en el hilo principal.
- 13:10 - Código no aislado
Una funcionalidad con @concurrent siempre cambiará de actor para ejecutarse. La palabra clave “nonisolated” permite elegir dónde ejecutar el código: en el hilo principal o en segundo plano. Para bibliotecas de propósito general, se recomienda ofrecer una API nonisolated y permitir que los clientes decidan si desean delegar el trabajo. Para más opciones de concurrencia, consulta “Más allá de los conceptos básicos de la concurrencia estructurada” de la WWDC23.
- 14:13 - Grupo de subprocesos concurrentes
Al descargar trabajo en segundo plano, el sistema administra la programación del trabajo en hilos en un grupo de hilos concurrentes. Los dispositivos más pequeños pueden tener menos subprocesos en el grupo, mientras que los sistemas grandes de más núcleos tendrán más. Las tareas se asignan a los hilos disponibles en el grupo, que pueden cambiar a medida que las tareas se suspenden y se reanudan, optimizando el uso de recursos.
- 14:58 - Compartir datos
Al trabajar con concurrencia y compartir datos entre hilos, existe el riesgo de introducir errores de tiempo de ejecución debido al acceso a un estado mutable compartido. El diseño de Swift mitiga este problema mediante verificaciones en tiempo de compilación, lo que permite a los desarrolladores escribir código concurrente con confianza.
- 15:49 - Tipos de valores
El uso de tipos de valores brinda una ventaja significativa al tratar con tareas simultáneas. Cuando se copia un tipo de valor en un hilo en segundo plano, se crea una copia independiente, lo que garantiza que cualquier cambio realizado en el hilo principal no afecte el valor del hilo en segundo plano. Esta independencia hace que los tipos de valores sean seguros para compartir entre hilos. Los tipos de valores que cumplen con el protocolo “Sendable” son siempre seguros para compartir en contextos concurrentes. Los tipos de actores principales son implícitamente Sendable.
- 17:16 - Tipos de actores aislados
Los actores Swift protegen el estado non-Sendable garantizando el acceso a una sola tarea. Cuando se envían valores hacia y desde los actores, el compilador Swift verifica la seguridad.
- 18:30 - Clases
En Swift, las clases son tipos por referencia, por lo que los cambios realizados en un objeto desde una variable son visibles desde todas las variables que apuntan a ese objeto. Cuando varios hilos acceden y modifican al mismo tiempo un objeto non-Sendable, pueden producirse carreras de datos, fallos o errores visuales. El sistema de concurrencia de Swift evita esto en tiempo de compilación al exigir que solo los tipos Sendable se compartan entre actores y tareas. Para evitar carreras de datos, es fundamental que los objetos mutables no se compartan concurrentemente. Completa las modificaciones a los objetos antes de enviarlos a otra tarea o actor para su visualización o procesamiento. Si un objeto necesita modificarse en un hilo en segundo plano, márcalo como “nonisolated”, pero no como Sendable. Los cierres con estado compartido también pueden ser seguros siempre que no se invoquen concurrentemente.
- 23:18 - Actores
A medida que una app crece, el actor principal puede administrar mucho estado, generando cambios de contexto. La introducción de actores puede mitigar esto. Los actores aíslan sus datos y permiten que solo un hilo a la vez acceda a ellos, evitando la contención. Al mover el estado del actor principal a actores dedicados, se puede ejecutar más código concurrentemente en subprocesos en segundo plano. Esto libera el hilo principal para mantener la capacidad de respuesta. Las clases orientadas a la IU y las clases de modelo deben permanecer en el actor principal o mantenerse como non-Sendable.
- 26:12 - Conclusión
Las apps suelen comenzar con un solo subproceso y evolucionan para usar tareas asincrónicas, código simultáneo y actores para un mejor rendimiento. La concurrencia en Swift facilita esta transición, simplificando el traslado del código fuera del hilo principal y mejorando la respuesta. Las herramientas de creación de perfiles como Instruments permiten identificar cuándo y qué código debe trasladarse fuera del hilo principal. Usa la configuración de compilación recomendada para simplificar la introducción de concurrencia, y la configuración de Concurrencia accesible para habilitar funcionalidades futuras que faciliten el trabajo.