From 4faebb5f005902963d398a1f295c9cd2a1e64a97 Mon Sep 17 00:00:00 2001 From: Chen Yang Date: Tue, 10 Jan 2023 09:44:36 -0800 Subject: [PATCH] Add guidance on Concurrency (#47) * Add guidance on Concurrency * Update the example to be more generalized * Fix grammer error --- Concurrency.md | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 85 insertions(+) create mode 100644 Concurrency.md diff --git a/Concurrency.md b/Concurrency.md new file mode 100644 index 0000000..6bd187b --- /dev/null +++ b/Concurrency.md @@ -0,0 +1,84 @@ +# Concurrency + +In general, follow the concurrency section in the [official guidelines](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html). + +## Convention + +Use a task to manage synchronous access to a property populated by an asynchronous function within an Actor. For more in-depth discussions of the provided example, watch [WWDC21 - Protect mutable state with Swift actors](https://developer.apple.com/wwdc21/10133). + +## Rationale +Even though Actors are helpful for sharing information between concurrent code, asynchronous functions in Actors do not guarantee the state of Actor properties after an asynchronous call. + +## Examples + +### Bad: when calling the function image(from:url:) across different threads, it's possible that multiple downloads are executed which is inefficient and unnecessary. If the download function always downloads to the same file path, the file path might contain partially downloaded contents. + +``` swift +actor ImageDownloader { + + func image(from url: URL) async throws -> UIImage { + if let cached = cache[url] { + return cached + } + + // When the following line executes, the function does not continue executing until + // the download finishes. During this time, a different thread could call the function + // and download the same image again. + let imageFilePath = try await downloadImage(from: url) + + // The image file might not contain the full downloaded contents. + let image = UIImage(contentsOfFile: imageFilePath) + + cache[url] = image + return image! + } + + private var cache: [URL: UIImage] = [:] +} + +``` + +### Good: Use a task that downloads the executable asynchronously. Subsequent requests of the same image will wait for the asynchronous task to finish if queued before the cache is populated or use the cached value after the task finishes. + +``` swift +actor ImageDownloader { + + /// Checks the cache for the image corresponding to the url + /// - Returns: the cached image if available, or wait for the task to finish and return the result of the task + func image(from url: URL) async throws -> UIImage { + if let cached = cache[url] { + switch cached { + case .ready(let image): + return image + case .inProgress(let task): + return try await task.value + } + } + + let task = Task { + let imageFilePath = try await downloadImage(from: url) + return UIImage(contentsOfFile: imageFilePath)! + } + + cache[url] = .inProgress(task) + + do { + let image = try await task.value + cache[url] = .ready(image) + return image + } catch { + cache[url] = nil + throw error + } + +} + + private var cache: [URL: CacheEntry] = [:] + + private enum CacheEntry { + case inProgress(Task) + case ready(UIImage) + } +} + +``` diff --git a/README.md b/README.md index 3b8f7e1..3806ec2 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ The Swift coding conventions guide documents many best practices for writing Swi * [Annotations and Modifiers](AnnotationsAndModifiers.md) * [Comments](Comments.md) +* [Concurrency](Concurrency.md) * [Delegates](Delegates.md) * [Early Return](EarlyReturn.md) * [File Organization](FileOrganization.md)