-
Adoptez la concurrence avec Swift
Rejoignez-nous pour découvrir les concepts fondamentaux de concurrence en Swift. La concurrence vous aide à améliorer la réactivité et les performances des apps, et Swift est conçu pour faciliter l'écriture sans erreur de code asynchrone et concurrent. Nous passerons en revue les étapes nécessaires pour faire évoluer une app d'un fonctionnement monothread à un fonctionnement concurrent. Nous vous aiderons également à déterminer comment et quand tirer le meilleur parti des fonctionnalités de concurrence en Swift, qu'il s'agisse de rendre votre code plus asynchrone, de le déplacer en arrière-plan ou de partager des données entre des tâches concurrentes.
Chapitres
- 0:00 - Introduction
- 3:17 - Code monothread
- 6:00 - Tâches asynchrones
- 7:24 - Entrelacement
- 10:22 - Présentation de la concurrence
- 11:07 - Fonctions concurrentes
- 13:10 - Code non isolé
- 14:13 - Pool de threads concurrents
- 14:58 - Partage des données
- 15:49 - Types de valeur
- 17:16 - Types isolés par un acteur
- 18:30 - Classes
- 23:18 - Acteurs
- 26:12 - Conclusion
Ressources
Vidéos connexes
WWDC25
WWDC23
-
Rechercher dans cette vidéo…
-
-
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 - Introduction
La simultanéité Swift permet aux apps d’exécuter plusieurs tâches à la fois, améliorant la réactivité et déchargeant les calculs en arrière-plan. Le modèle de simultanéité Swift facilite l’écriture correcte du code concurrent en rendant la gestion de la concurrence claire, en identifiant les données partagées entre tâches, et en détectant les conflits de données lors de la compilation. Les apps commencent par exécuter tout le code sur le thread principal. À mesure que la complexité augmente, on peut introduire des tâches asynchrones pour les opérations longues comme l’accès réseau. Les threads en arrière-plan sont utilisés pour les tâches plus lourdes en calcul. Swift fournit des outils tels que des acteurs et des tâches pour gérer ces opérations simultanées.
- 3:17 - Code monothread
En Swift, le code monothread s’exécute sur le thread principal, isolé de l’acteur principal. Il n’y a pas de simultanéité sur l’acteur principal, car un seul thread peut l’exécuter. Les données ou le code peuvent être placés sur l’acteur principal via la notation @MainActor. Swift veille à ce que le code de l’acteur principal ne s’exécute que sur le thread principal et que ses données ne soient accessibles qu’à partir de là. On dit alors que ce code est isolé de l’acteur principal. Par défaut, Swift protège le code du thread principal avec l’acteur principal, ce qui permet d’accéder librement à l’état partagé. La protection du code avec l’acteur principal est pilotée par défaut par un paramètre de build. Utilisez-le principalement pour le module principal de l’app et les modules liés à I’UI.
- 6:00 - Tâches asynchrones
La simultanéité Swift utilise async et await pour rendre les fonctions non bloquantes, ce qui permet d’exécuter d’autres tâches en attendant des données. Cela évite les blocages et améliore la réactivité de l’UI en séparant les fonctions avant et après l’évènement attendu.
- 7:24 - Entrelacement
Les fonctions asynchrones s’exécutent dans une tâche, indépendamment des autres. Un seul thread peut alterner entre des tâches prêtes à s’exécuter grâce à l’entrelacement. Cela améliore les performances en évitant les temps morts et en optimisant l’utilisation des ressources. Les tâches asynchrones sont efficaces pour exécuter de nombreuses opérations indépendantes en même temps. Pour exécuter des tâches dans un ordre précis, utilisez une seule tâche. Des tâches asynchrones sur un seul thread suffisent souvent. Si le thread principal est surchargé, des outils comme Instruments permettent d’identifier les goulots d’étranglement à optimiser avant de recourir à la simultanéité.
- 10:22 - Présentation de la concurrence
La simultanéité permet d’exécuter du code en arrière-plan en parallèle du thread principal, en tirant parti des cœurs CPU pour accélérer le travail. Pour améliorer les performances, l’exemple utilise la simultanéité pour exécuter du code en arrière-plan et libérer le thread principal.
- 11:07 - Fonctions concurrentes
En appliquant l’attribut @concurrent, Swift est invité à exécuter une fonction en arrière-plan. Le compilateur Swift signale les accès aux données via l’acteur principal pour introduire la simultanéité en toute sécurité. Une bonne pratique consiste à déplacer le code de l’acteur principal dans un appelant qui s’exécute toujours sur le thread principal.
- 13:10 - Code non isolé
Une fonction @concurrent se déconnectera toujours d’un acteur pour s’exécuter. Le mot-clé nonisolated permet aux clients de choisir où exécuter le code : sur le thread principal ou en arrière-plan. Pour les bibliothèques à usage général, mieux vaut proposer une API nonisolated et laisser le client décider de décharger le travail ou non. Pour aller plus loin, regardez « Beyond the basics of structured concurrency » de la WWDC23.
- 14:13 - Pool de threads concurrents
Lorsque le travail est délégué en arrière-plan, le système le planifie sur des threads d’un pool concurrent. Les petits appareils ont moins de threads dans le pool, tandis que les systèmes plus puissants en ont davantage. Les tâches sont affectées aux threads disponibles, qui peuvent varier en fonction des pauses et des reprises des tâches, pour optimiser les ressources.
- 14:58 - Partage des données
Avec la simultanéité, partager des données entre threads peut provoquer des bugs à l’exécution si l’état mutable est mal géré. Swift limite ce risque grâce à des vérifications à la compilation, ce qui aide les développeurs à écrire du code concurrent en toute confiance.
- 15:49 - Types de valeur
L’utilisation des types valeur offre un réel avantage avec les tâches simultanées. Quand un type valeur est copié en arrière-plan, il devient indépendant : les changements sur le thread principal n’ont aucun impact sur lui. Cette indépendance permet de partager les types valeur en toute sécurité entre les threads. Les types valeur conformes au protocole Sendable peuvent toujours être partagés en toute sécurité. Les types liés à l’acteur principal sont implicitement Sendable.
- 17:16 - Types isolés par un acteur
Les acteurs Swift protègent l’état non Sendable en garantissant un accès à une seule tâche. Lorsque des valeurs circulent entre acteurs, le compilateur Swift en vérifie la sécurité.
- 18:30 - Classes
En Swift, les classes sont des types référence : toute modification via une variable est visible par toutes les autres qui pointent vers le même objet. Quand plusieurs threads modifient un objet non-Sendable en même temps, cela peut provoquer des conflits, des bugs ou des pannes. Le système de simultanéité de Swift empêche cela à la compilation en imposant que seuls les types Sendable soient partagés entre acteurs et tâches. Pour éviter les conflits d’accès, il est crucial de ne pas partager d’objets mutables simultanément. Terminez les modifications sur un objet avant de l’envoyer à une autre tâche ou acteur pour affichage ou traitement. Si un objet doit être modifié sur un thread d’arrière-plan, rendez-le nonisolated, sans le rendre Sendable. Les fermetures avec état partagé peuvent être sûres, tant qu’elles ne sont pas appelées simultanément.
- 23:18 - Acteurs
À mesure que l’app grandit, l’acteur principal gère beaucoup d’états, entrainant des changements fréquents de contexte. L’introduction d’acteurs peut atténuer cela. Les acteurs isolent leurs données, accessibles par un seul thread à la fois, évitant ainsi les conflits. En déplaçant l’état vers des acteurs dédiés, plus de code peut s’exécuter en parallèle sur des threads d’arrière-plan. Cela libère le thread principal pour maintenir la réactivité. Les classes liées à l’UI ou au modèle doivent rester sur l’acteur principal ou être non-Sendable.
- 26:12 - Conclusion
Les apps commencent souvent en monothread, puis évoluent vers l’asynchrone, la concurrence et les acteurs pour gagner en performance. La simultanéité Swift facilite cette transition en facilitant le déplacement du code hors du thread principal et en améliorant la réactivité. Les outils de profilage comme Instruments aident à identifier quand et quel code doit être retiré du thread principal. Utilisez les réglages de build recommandés pour faciliter l’ajout de la simultanéité, et activez l’option Approachable Concurrency pour profiter de nouvelles fonctionnalités qui en simplifient l’usage.