├── .github └── FUNDING.yml ├── Sources ├── ConcurrencyRecipes │ └── ConcurrencyRecipes.swift └── PreconcurrencyLib │ └── PreconcurrencyStuff.swift ├── .gitignore ├── Package.swift ├── Recipes ├── Interoperability.md ├── SwiftUI.md ├── Isolation.md ├── PreconcurrencyLibraries.md ├── AsyncContext.md ├── Structured.md └── Protocols.md ├── LICENSE ├── Tests └── ConcurrencyRecipesTests │ └── PreconcurrencyLibTests.swift ├── README.md └── CODE_OF_CONDUCT.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [mattmassicotte] 2 | -------------------------------------------------------------------------------- /Sources/ConcurrencyRecipes/ConcurrencyRecipes.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | 3 | import PackageDescription 4 | 5 | let settings: [SwiftSetting] = [ 6 | .enableExperimentalFeature("StrictConcurrency") 7 | ] 8 | 9 | let package = Package( 10 | name: "ConcurrencyRecipes", 11 | products: [ 12 | // .library( 13 | // name: "ConcurrencyRecipes", 14 | // targets: ["ConcurrencyRecipes"] 15 | // ), 16 | ], 17 | targets: [ 18 | .target(name: "PreconcurrencyLib"), 19 | // .target( 20 | // name: "ConcurrencyRecipes", 21 | // swiftSettings: settings 22 | // ), 23 | .testTarget( 24 | name: "ConcurrencyRecipesTests", 25 | dependencies: ["PreconcurrencyLib"], 26 | swiftSettings: settings 27 | ), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Recipes/Interoperability.md: -------------------------------------------------------------------------------- 1 | # Interoperability 2 | 3 | Using Swift concurrency with other concurrency systems. 4 | 5 | ## DispatchQueue.async 6 | 7 | This is kind of a meme on Apple platforms, and it can be a hack. But sometimes you really do need to just let the runloop turn. Just be sure you have a good rationale for why it is necessary. 8 | 9 | ```swift 10 | // work before the runloop has turned... 11 | 12 | DispatchQueue.main.async { 13 | // ... and need to do stuff after it has finished... 14 | } 15 | 16 | // ... because state out of your control isn't yet right here 17 | ``` 18 | 19 | ### Solution #1: Use a continuation 20 | 21 | To make this work in an async context, you can use a continuation. 22 | 23 | ```swift 24 | // work before the runloop has turned... 25 | 26 | await withCheckedContinuation { continuation in 27 | DispatchQueue.main.async { 28 | continuation.resume() 29 | } 30 | } 31 | 32 | // at this point, you are **guaranteed** that the main runloop has turned at least once 33 | ``` 34 | -------------------------------------------------------------------------------- /Sources/PreconcurrencyLib/PreconcurrencyStuff.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func acceptEscapingBlock(_ block: @escaping () -> Void) { 4 | DispatchQueue.global().async { 5 | block() 6 | } 7 | } 8 | 9 | public func unannotatedEscapingSendableBlock(_ block: @escaping @Sendable () -> Void) { 10 | DispatchQueue.global().async { 11 | block() 12 | } 13 | } 14 | 15 | @preconcurrency 16 | public func annotatedEscapingSendableBlock(_ block: @escaping @Sendable () -> Void) { 17 | DispatchQueue.global().async { 18 | block() 19 | } 20 | } 21 | 22 | public class NonSendableClass { 23 | var mutableState: Int = 0 24 | 25 | public init() { 26 | } 27 | 28 | public func accessMutableState() { 29 | print("state:", mutableState) 30 | } 31 | 32 | public func asyncFunction() async { 33 | 34 | } 35 | } 36 | 37 | @preconcurrency 38 | public class AnnotatedNonSendableClass { 39 | var mutableState: Int = 0 40 | 41 | public init() { 42 | } 43 | 44 | public func accessMutableState() { 45 | print("state:", mutableState) 46 | } 47 | } 48 | 49 | public final class SendableClass: Sendable { 50 | public init() { 51 | } 52 | } 53 | 54 | public protocol NonIsolatedProtocol { 55 | func nonSendableCallback(callback: @escaping () -> Void) 56 | 57 | // @preconcurrency 58 | func annotatedNonSendableCallback(callback: @escaping @Sendable () -> Void) 59 | } 60 | -------------------------------------------------------------------------------- /Recipes/SwiftUI.md: -------------------------------------------------------------------------------- 1 | # SwiftUI 2 | 3 | SwiftUI has a lot of built-in support for concurrency. 4 | 5 | ## Non-MainActor Isolation 6 | 7 | Wait! As of the SDKs in Xcode 16, Swift's [`View`](https://developer.apple.com/documentation/swiftui/view) now is isolated to the `MainActor`!. However, just in case you want to have a look at what was required before, here are some ideas. 8 | 9 | ### Solution #1: Just use MainActor 10 | 11 | Just slap an `@MainActor` on there to make things much easier. 12 | 13 | ```swift 14 | @MainActor 15 | struct MyView: View { 16 | var body: some View { 17 | Text("Body") 18 | } 19 | } 20 | ``` 21 | 22 | ### Solution #2: Override View 23 | 24 | This is a little more radical of a solution, but can save lots of typing. Swift makes it possible to override entire types within a module. So, you'd have to do this in any module directly, but it certainly does reduce the amount of boilerplate. I'd be a little careful with this one, but it is also ok to add redundant global actor annotations, so at least it is trivial to remove. 25 | 26 | ```swift 27 | // do this once 28 | @MainActor 29 | protocol View: SwiftUI.View { 30 | @ViewBuilder 31 | var body: Self.Body { get } 32 | } 33 | 34 | // inherit MainActor isolation within the same module 35 | struct MyView: View { 36 | var body: some View { 37 | Text("Body") 38 | } 39 | } 40 | ``` 41 | 42 | ## EnvironmentKey/PreferencesKey 43 | 44 | These can be hard to use! Check out the [protocol section](./Protocols.md) for ideas. 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Matt Massicotte 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Tests/ConcurrencyRecipesTests/PreconcurrencyLibTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | //import PreconcurrencyLib 3 | @preconcurrency import PreconcurrencyLib 4 | 5 | final class PreconcurrencyLibTests: XCTestCase { 6 | func testAcceptEscapingBlock() { 7 | let ns = NonSendableClass() 8 | 9 | acceptEscapingBlock { 10 | print("hello") 11 | 12 | // no warning because this block isn't sendable 13 | ns.accessMutableState() 14 | } 15 | } 16 | 17 | func testUnannotatedEscapingSendableBlock() { 18 | let ns = NonSendableClass() 19 | 20 | unannotatedEscapingSendableBlock { 21 | print("hello") 22 | 23 | // warning, block is now Sendable 24 | ns.accessMutableState() 25 | } 26 | } 27 | 28 | func testAnnotatedEscapingSendableBlock() { 29 | let ns = NonSendableClass() 30 | 31 | annotatedEscapingSendableBlock { 32 | print("hello") 33 | 34 | // warning, block is now Sendable 35 | ns.accessMutableState() 36 | } 37 | 38 | DispatchQueue.global().async { 39 | ns.accessMutableState() 40 | } 41 | } 42 | 43 | func testAccessNonSendableClass() { 44 | let ns = NonSendableClass() 45 | 46 | DispatchQueue.global().async { 47 | ns.accessMutableState() 48 | } 49 | } 50 | 51 | func testAnnotatedAccessNonSendableClass() { 52 | let ns = AnnotatedNonSendableClass() 53 | 54 | DispatchQueue.global().async { 55 | ns.accessMutableState() 56 | } 57 | } 58 | 59 | @MainActor 60 | func testNonisolatedAsyncMethodOfNonSendableClass() async { 61 | // created in MainActor domain 62 | let ns = NonSendableClass() 63 | 64 | // crosses to non-isolated here 65 | await ns.asyncFunction() 66 | } 67 | } 68 | 69 | extension PreconcurrencyLibTests { 70 | static let nonSendableConstant = NonSendableClass() 71 | static let annotatedNonSendableConstant = AnnotatedNonSendableClass() 72 | static let sendableContant = SendableClass() 73 | 74 | nonisolated(unsafe) static let unsafeSendableContant = SendableClass() 75 | nonisolated(unsafe) static let unsafeNonSendableConstant = NonSendableClass() 76 | } 77 | 78 | @MainActor 79 | final class AdoptNonIsolatedProtocol: NonIsolatedProtocol { 80 | func work() async { 81 | 82 | } 83 | 84 | nonisolated func nonSendableCallback(callback: @escaping @Sendable () -> Void) { 85 | Task { 86 | await self.work() 87 | callback() 88 | } 89 | } 90 | 91 | nonisolated func annotatedNonSendableCallback(callback: @escaping @Sendable () -> Void) { 92 | Task { 93 | await self.work() 94 | 95 | await MainActor.run { 96 | callback() 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ConcurrencyRecipes 2 | Practical solutions to problems with Swift Concurrency 3 | 4 | Swift Concurrency can be really hard to use. I thought it could be handy to document and share solutions and hazards you might face along the way. I am **absolutely not** saying this is comprehensive, or that the solutions presented are great. I'm learning too. Contributions are very welcome, especially for problems! 5 | 6 | > [!WARNING] 7 | > I have not updated the code in this repo for Swift 6.0. And on top of changes to the language, which were substantial, my general understandings/opinions have changed as well. I'm still absolutely interested in your feedback/issue reports! But, please keep all this in mind when looking through the repo. 8 | 9 | ## Table of Contents 10 | 11 | - [Creating an Async Context](Recipes/AsyncContext.md) 12 | - [Using Protocols](Recipes/Protocols.md) 13 | - [Isolation](Recipes/Isolation.md) 14 | - [Structured Concurrency](Recipes/Structured.md) 15 | - [SwiftUI](Recipes/SwiftUI.md) 16 | - [Using Libraries not Designed for Concurrency](Recipes/PreconcurrencyLibraries.md) 17 | - [Interoperability](Recipes/Interoperability.md) 18 | 19 | ## Hazards 20 | 21 | Quick definitions for the hazards referenced throughout the recipes: 22 | 23 | - Timing: More than one option is available, but can affect when events actually occur. 24 | - Ordering: Unstructured tasks means ordering is up to the caller. Think carefully about dependencies, multiple invocations, and cancellation. 25 | - Lack of Caller Control: definitions always control actor context. This is different from other threading models, and you cannot alter definitions you do not control. 26 | - Sendability: types that cross isolation domains must be sendable. This isn't always easy, and for types you do not control, not possible. 27 | - Blocking: Swift concurrency uses a fixed-size thread pool. Tying up background threads can lead to lag and even deadlock. 28 | - Availability: Concurrency is evolving rapidly, and some APIs require the latest SDK. 29 | - Async virality: Making a function async affects all its callsites. This can result in a large number of changes, each of which could, itself, affect subsequence callsites. 30 | - Actor Reentrancy: More than one thread can enter an Actor's async methods. An actor's state can change across awaits. 31 | 32 | ## Contributing and Collaboration 33 | 34 | I would love to hear from you! Issues or pull requests work great. You can also find me on [the web](https://www.massicotte.org). 35 | 36 | I prefer collaboration, and would love to find ways to work together if you have a similar project. 37 | 38 | I prefer indentation with tabs for improved accessibility. But, I'd rather you use the system you want and make a PR than hesitate because of whitespace. 39 | 40 | By participating in this project you agree to abide by the [Contributor Code of Conduct](CODE_OF_CONDUCT.md). 41 | -------------------------------------------------------------------------------- /Recipes/Isolation.md: -------------------------------------------------------------------------------- 1 | # Isolation 2 | 3 | Passing data around across isolation domains means you need the types to conform to [Sendable](https://developer.apple.com/documentation/swift/sendable). 4 | 5 | ## Non-Sendable Arguments 6 | 7 | You need to pass some non-Sendable arguments into a function in a different isolation domain. 8 | 9 | ```swift 10 | func myAsyncFunction(_ nonSendable: NonSendable) async { 11 | } 12 | 13 | let nonSendable = NonSendable() 14 | 15 | // this produces a warning 16 | await myAsyncFunction(nonSendable) 17 | ``` 18 | 19 | ### Solution #1: create data in a closure 20 | 21 | Assumption: the definition is under your control. 22 | 23 | ```swift 24 | func myAsyncFunction(_ nonSendable: @Sendable () -> NonSendable) async { 25 | } 26 | 27 | await myAsyncFunction({ NonSendable() }) 28 | ``` 29 | 30 | ## Variable Actor Isolation 31 | 32 | You need to isolate things differently depending on usage. 33 | 34 | ```swift 35 | func takesClosure(_ block: () -> Void) { 36 | } 37 | 38 | takesClosure { 39 | accessMainActorOnlyThing() 40 | } 41 | ``` 42 | 43 | ### Solution #1: assumeIsolated 44 | 45 | We've seen this one before when working with protocols. 46 | 47 | ```swift 48 | func takesClosure(_ block: () -> Void) { 49 | } 50 | 51 | takesClosure { 52 | MainActor.assumeIsolated { 53 | accessMainActorOnlyThing() 54 | } 55 | } 56 | ``` 57 | 58 | ### Solution #2: actor-specific version 59 | 60 | If you find yourself doing this a lot or you are just not into the nesting, you can make wrapper. 61 | 62 | ```swift 63 | func takesMainActorClosure(_ block: @MainActor () -> Void) { 64 | takesClosure { 65 | MainActor.assumeIsolated { 66 | block() 67 | } 68 | } 69 | } 70 | 71 | takesMainActorClosure { 72 | accessMainActorOnlyThing() 73 | } 74 | ``` 75 | 76 | ### Solution #3: isolated parameter 77 | 78 | ```swift 79 | func takesClosure(isolatedTo actor: isolated any Actor, block: () -> Void) { 80 | } 81 | ``` 82 | 83 | ## Custom Global Actors 84 | 85 | There are situations where you need to manage a whole bunch of global state all together. In a case like that, a custom global actor can be useful. 86 | 87 | ## Making an Actor 88 | 89 | ```swift 90 | @globalActor 91 | public actor CustomGlobalActor { 92 | public static let shared = CustomGlobalActor() 93 | 94 | // I wanted to do something like MainActor.assumeIsolated, but it turns out every global actor has to implement that manually. This is because 95 | // it isn't possible to express a global actor assumeIsolated generically. So I just copied the sigature from MainActor. 96 | public static func assumeIsolated(_ operation: @CustomGlobalActor () throws -> T, file: StaticString = #fileID, line: UInt = #line) rethrows -> T { 97 | // verify that we really are in the right isolation domain 98 | Self.shared.assertIsolated() 99 | 100 | // use some tricky casting to remove the global actor so we can execute the closure 101 | return try withoutActuallyEscaping(operation) { fn in 102 | try unsafeBitCast(fn, to: (() throws -> T).self)() 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | ## Async Methods on Non-Sendable Types 109 | 110 | Non-`Sendable` types **can** participate in concurrency. But, because `self` cannot cross isolation domains, it's easy to accidentally make the type unusable from an isolated context. 111 | 112 | ```swift 113 | class NonSendableType { 114 | func asyncFunction() async { 115 | } 116 | } 117 | 118 | @MainActor 119 | class MyMainActorClass { 120 | // this value is isolated to the MainActor 121 | let value = NonSendableType() 122 | 123 | func useType() async { 124 | // here value is being transferred from the MainActor to a non-isolated 125 | // context. That's not allowed. 126 | // ERROR: Sending 'self.value' risks causing data races 127 | await value.asyncFunction() 128 | } 129 | } 130 | ``` 131 | 132 | ### Solution #1: isolated parameter 133 | 134 | ```swift 135 | class NonSendableType { 136 | func asyncFunction(isolation: isolated (any Actor)? = #isolation) async { 137 | } 138 | } 139 | 140 | @MainActor 141 | class MyMainActorClass { 142 | // this value is isolated to the MainActor 143 | let value = NonSendableType() 144 | 145 | func useType() async { 146 | // the compiler now knows that isolation does not change for 147 | // this call, which makes it possible. 148 | await value.asyncFunction() 149 | } 150 | } 151 | ``` 152 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | support@chimehq.com. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /Recipes/PreconcurrencyLibraries.md: -------------------------------------------------------------------------------- 1 | # Using Libraries not Designed for Concurrency 2 | 3 | You are using a Swift library that wasn't correctly built for Swift concurrency and things are going wrong. 4 | 5 | ## Capturing Non-Sendable Types 6 | 7 | You need to pass a type from this library to a `@Sendable` closure. Just remember, nothing will make this magically safe and you still should be confident you are not introducing data races. 8 | 9 | ```Swift 10 | import TheLibrary 11 | 12 | func useTheType() { 13 | let value = TypeFromTheLibrary() 14 | 15 | Task { 16 | value.doStuff() // WARNING: Capture of 'value' with non-sendable... 17 | } 18 | } 19 | ``` 20 | 21 | ## Solution #1: `@preconcurrency` 22 | 23 | This is an easy one. You can just import the library with `@preconcurrency`. 24 | 25 | ```Swift 26 | @preconcurrency import TheLibrary 27 | 28 | func useTheType() { 29 | let value = TypeFromTheLibrary() 30 | 31 | Task { 32 | value.doStuff() 33 | } 34 | } 35 | ``` 36 | 37 | ## Solution #2: use static isolation 38 | 39 | This addresses the issue because it forces all accesses to be isolated to a single actor. 40 | 41 | ```Swift 42 | import TheLibrary 43 | 44 | @MainActor 45 | func useTheType() { 46 | let value = TypeFromTheLibrary() 47 | 48 | Task { 49 | value.doStuff() 50 | } 51 | } 52 | ``` 53 | 54 | ## Solution #3: use dynamic isolation 55 | 56 | This is a more-flexible version of #2. Note that is doesn't work as of Swift 5.10, but hopefully will [soon](https://forums.swift.org/t/isolation-assumptions/69514)!. 57 | 58 | ```Swift 59 | import TheLibrary 60 | 61 | func useTheType(isolatedTo actor: any Actor) { 62 | let value = TypeFromTheLibrary() 63 | 64 | Task { 65 | value.doStuff() 66 | } 67 | } 68 | ``` 69 | 70 | ## Initializing Static Variables 71 | 72 | You need to use a type from this library to initialize a static variable. 73 | 74 | ```Swift 75 | import TheLibrary 76 | 77 | class YourClass { 78 | static let value = TypeFromTheLibrary() // WARNING: Static property 'value' is not concurrency-safe... 79 | } 80 | ``` 81 | 82 | ## Solution #1: `nonisolated(unsafe)` 83 | 84 | This construct was introduced specifically to handle this situation. It's worth noting that `@preconcurrency import` does affect this behavior: it will suppress any **errors** related to isolation checking by turning them into warnings. 85 | 86 | ```Swift 87 | import TheLibrary 88 | 89 | class YourClass { 90 | nonisolated(unsafe) static let value = TypeFromTheLibrary() 91 | } 92 | ``` 93 | 94 | ## Protocol Function with Callback 95 | 96 | You have a protocol that uses callbacks. These callbacks are not correctly marked with global actors or `@Sendable`. 97 | 98 | ```Swift 99 | import TheLibrary 100 | 101 | class YourClass: LibraryProtocol { 102 | func protocolFunction(callback: @escaping () -> Void) 103 | Task { 104 | // doing your async work here 105 | 106 | // WARNING: Capture of 'callback' with non-sendable type '() -> Void' in a `@Sendable` closure 107 | callback() 108 | } 109 | } 110 | ``` 111 | 112 | ## Solution #1: `@preconcurrency` + `@Sendable` 113 | 114 | If you import the library with `@preconcurrency`, you can adjust your conformance to match the `@Sendable` reality of the function. 115 | 116 | ```Swift 117 | @preconcurrency import TheLibrary 118 | 119 | class YourClass: LibraryProtocol { 120 | // the callback is documented to actually be ok to call on any thread, so it must be @Sendable. With preconcurrency, this Sendable mismatch is ok. 121 | func protocolFunction(callback: @escaping @Sendable () -> Void) 122 | Task { 123 | // doing your async work here 124 | 125 | callback() 126 | } 127 | } 128 | ``` 129 | 130 | ## Solution #2: `@preconcurrency` + `@Sendable` + `MainActor.run` 131 | 132 | Almost the same as #1, but the callback must be run on the main actor. In this case, it is not possible to add `@MainActor` to the conformance, and you have to instead make the isolation manual. 133 | 134 | ```Swift 135 | @preconcurrency import TheLibrary 136 | 137 | class YourClass: LibraryProtocol { 138 | // the callback is documented to actually be ok to call on any thread, so it must be @Sendable. With preconcurrency, this mismatch is still considered a match. 139 | func protocolFunction(callback: @escaping @Sendable () -> Void) 140 | Task { 141 | // doing your async work here 142 | 143 | // ensure you are back on the MainActor here 144 | await MainActor.run { 145 | callback() 146 | } 147 | } 148 | } 149 | ``` 150 | 151 | ## Converting a completion callback to async 152 | 153 | You want to convert a function that uses a callback to async. This is particularly common when interoperating with Objective-C code, and the compiler will even generate async versions of certain patterns automically. But isolation and Sendability can still be a problem. 154 | 155 | ```swift 156 | func doWork(argument: ArgValue, _ completionHandler: @escaping (ResultValue) -> Void) 157 | ``` 158 | 159 | ## Solution #1: plain wrapper 160 | 161 | If the arguments (`ArgValue`) and return value (`ResultValue`) are `Sendable`, this is very straightforward. Remember, though, that just putting async on a function without any isolation makes it non-isolated. You must make sure that's compatible with how the function you are wrapping works. 162 | 163 | ```swift 164 | func doWork() async -> ResultValue { 165 | await withCheckedContinuation { continuation in 166 | doWork { value in 167 | continuation.resume(returning: value) 168 | } 169 | } 170 | } 171 | ``` 172 | 173 | ## Solution #2: wrapper with non-Sendable return value 174 | 175 | A non-Sendable return value can potentially be a showstopper. But, if you do not need the entire object, or can transform it in some way before returning it, you can pull this off. 176 | 177 | ```swift 178 | func doWork() async -> ResultValue { 179 | await withCheckedContinuation { continuation in 180 | doWork { nonSendableValue in 181 | // within this block, it's safe to synchronously access the non-Sendable value 182 | let sendableThing = nonSendableValue.partYouReallyNeed 183 | 184 | continuation.resume(returning: sendableThing) 185 | } 186 | } 187 | } 188 | ``` 189 | -------------------------------------------------------------------------------- /Recipes/AsyncContext.md: -------------------------------------------------------------------------------- 1 | # Async Contexts 2 | 3 | Virtually all programs need to make at least one transition from a synchronous context to an asynchronous one. 4 | 5 | ## Ad-Hoc 6 | 7 | You need to call some async function from a synchronous one. 8 | 9 | ```swift 10 | func work() async throws { 11 | } 12 | ``` 13 | 14 | ### Solution #1: Plain Unstructured Task 15 | 16 | ```swift 17 | // Hazard 1: Ordering 18 | Task { 19 | // Hazard 2: thrown errors are invisible 20 | try await work() 21 | } 22 | ``` 23 | 24 | ### Solution #2: Typed Unstructured Task 25 | 26 | Adding explicit return/error types to the `Task` will make it impossible to accidentally ignore thrown errors. 27 | 28 | ```swift 29 | // Hazard 1: Ordering 30 | Task { 31 | try await work() 32 | } 33 | ``` 34 | 35 | ## Background Work 36 | 37 | This is an extremely common pattern in Swift code: You need to kick off some work from the main thread, complete it in the background, and then update some state back on the main thread. For example, you might need to add a spinner and disable some buttons in the "before" stage, do an expensive computation in the "background" stage, then update the UI again in the "after" stage. Using DispatchQueues, you could write the code like this: 38 | 39 | ```swift 40 | final class DemoViewController: UIViewController { 41 | func doWork() { 42 | // We assume we're on the main thread here 43 | beforeWorkBegins() 44 | 45 | DispatchQueue.global().async { 46 | let possibleResult = expensiveWork(arguments) 47 | 48 | DispatchQueue.main.async { 49 | afterWorkIsDone(possibleResult) 50 | } 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | This DispatchQueue pattern provides the following properties: 57 | 58 | 1. **Ordering**: You know `beforeWorkBegins()` runs before `expensiveWorks()` which runs before `afterWorkIsDone()`. 59 | 2. **Thread-safety**: You know `beforeWorkBegins()` and `afterWorkIsDone()` run on the main thread and `expensiveWork()` runs on a background thread. 60 | 3. **"Immediacy"**: There is no "waiting" before running `beforeWorkBegins()`. 61 | 62 | There are different recipes for recreating this pattern in Swift Concurrency and preserving these properties depending upon: 63 | 64 | 1. Does an asynchronous wrapper exist for `expensiveWork()`? 65 | 2. Does the _immediacy_ property need to be preserved for a synchronous or asynchronous context? 66 | 67 | ### Solution #1: Asynchronous wrapper exists & "synchronous immediacy" 68 | 69 | This recipe assumes there is an asynchronous wrapper for `expensiveWork()` and ensures that `beforeWorkBegins()` runs without any waiting in the caller's synchronous context. 70 | 71 | ```swift 72 | final class DemoViewController: UIViewController { 73 | func doWork() { 74 | // hazard 1: timing. 75 | // 76 | // If you moved this to inside the `Task`, you'd introduce a "wait" before 77 | // executing `beforeWorkBegins()`, losing the "immediacy" property. 78 | beforeWorkBegins() 79 | 80 | // hazard 2: ordering 81 | // 82 | // While this recipe guarantees that `beforeWorkBegins()` happens before 83 | // `asyncExpensiveWork()`, if there are any **other** Tasks that are 84 | // created by the caller, there is no ordering guarantees among the tasks. 85 | // This differs from the DispatchQueue.global().async world, where blocks 86 | // are started in the order in which they are submitted to the queue. 87 | Task { 88 | // MainActor-ness has been inherited from the creating context. 89 | // hazard 3: lack of caller control 90 | // hazard 3: sendability (for both `result and `arguments`) 91 | let result = await asyncExpensiveWork(arguments) 92 | // post-await we are now back on the original, MainActor context 93 | afterWorkIsDone(result) 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | ### Solution #2: Asynchronous wrapper exists & "asynchronous immediacy" 100 | 101 | If all of the callers of `doWork()` are already in asynchronous contexts, or if the callers can easily be made asynchronous (beware the "async virality" hazard), you can use this recipe: 102 | 103 | ```swift 104 | final class DemoViewController: UIViewController { 105 | // hazard 1: async virality. Can you reasonably change all callsites to `async`? 106 | func doWork() async { 107 | beforeWorkBegins() 108 | let result = await asyncExpensiveWork(arguments) 109 | afterWorkIsDone(result) 110 | } 111 | } 112 | ``` 113 | 114 | This recipe preserves immediacy of `beforeWorkBegins()` for any asynchronous callers and avoids introducing additional unstructured `Task` operations. 115 | 116 | ### Solution #3: No async wrapper exists for `expensiveWork()` 117 | 118 | If there is no `async` wrapper for `expensiveWork()`, you cannot directly use Solutions 1 or 2. 119 | 120 | One option you have: Write your own `async` wrapper, then proceed with Solution 1 or 2! 121 | 122 | ```swift 123 | func asyncExpensiveWork(arguments: Arguments) async -> Result { 124 | await withCheckedContinuation { continuation in 125 | // Hazard: Are you using the appropriate quality of service queue? 126 | DispatchQueue.global.async { 127 | let result = expensiveWork(arguments) 128 | continuation.resume(returning: result) 129 | } 130 | } 131 | } 132 | ``` 133 | 134 | Yes, this just sneaks `DispatchQueue.global.async` into the Swift Concurrency world. However, this seems appropriate: You won't tie up one of the threads in the cooperative thread pool to execute `expensiveWork()`. Another advantage of this approach: It's now baked into the implementation of `asyncExpensiveWork()` that the expensive stuff happens on a background thread. You can't accidentally run the code in the main actor context. 135 | 136 | **Sidenote** To Swift, `func foo()` and `func foo() async` are different and the compiler knows which one to use depending on if the callsite is a synchronous or asynchronous context. This means your async wrappers can have the same naming as their synchronous counterparts: 137 | 138 | ```swift 139 | func expensiveWork(arguments: Arguments) async -> Result { 140 | await withCheckedContinuation { continuation in 141 | DispatchQueue.global().async { 142 | let result = expensiveWork(arguments) 143 | continuation.resume(returning: result) 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | ### Solution #4: No async wrapper exists and you don't want to write one 150 | 151 | ```swift 152 | final class DemoViewController: UIViewController { 153 | func doWork() { 154 | // Work must start here, because we're going to explicitly hop off the MainActor 155 | beforeWorkBegins() 156 | 157 | // hazard 1: ordering 158 | Task.detached { 159 | // hazard 2: blocking 160 | // hazard 3: sendability (arguments) 161 | let possibleResult = expensiveWork(arguments) 162 | 163 | // hazard 4: sendability (possibleResult) 164 | await MainActor.run { 165 | afterWorkIsDone(possibleResult) 166 | } 167 | } 168 | } 169 | } 170 | ``` 171 | 172 | ## Order-Dependent Work 173 | 174 | You need to call some async function from a synchronous one **and** ordering must be preserved. 175 | 176 | ```swift 177 | func work() async throws { 178 | } 179 | ``` 180 | 181 | ### Solution #1: use AsyncStream as a queue 182 | 183 | ```swift 184 | // define a sequence that models the work (can be less-general than a function) 185 | typealias WorkItem = @Sendable () async throws -> Void 186 | 187 | let (stream, continuation) = AsyncStream.makeStream() 188 | 189 | // begin enumerating the sequence with a single Task 190 | // hazard 1: this Task will run at a fixed priority 191 | Task { 192 | // the sequence guarantees order 193 | for await workItem in stream { 194 | try? await workItem() 195 | } 196 | } 197 | 198 | // the continuation provides access to an async context 199 | // Hazard 2: per-work item cancellation is unsupported 200 | continuation.yield({ 201 | // Hazard 3: thrown errors are invisible 202 | try await work() 203 | }) 204 | ``` 205 | 206 | ## Program Entry 207 | 208 | This one is less-common for app developers, but handy for CLI tools and other programs that need a programmer-defined entry point. 209 | 210 | ### Solution #1: Top-Level Code 211 | 212 | Swift's top level code allows you to both throw *and* await. 213 | 214 | ```swift 215 | // swift test.swift 216 | 217 | func doThing() async throws { 218 | try await Task.sleep(nanoseconds: 1_000_000_000) 219 | } 220 | 221 | try await doThing() 222 | 223 | print("finished") 224 | ``` 225 | 226 | ### Solution #2: @main 227 | 228 | Remember that Swift does not support using `@main` with a file that is inferred to have top-level code. It's confusing, but does make sense as this would result in an ambiguous start point. 229 | 230 | ```swift 231 | // swiftc -parse-as-library test.swift 232 | // ./test 233 | 234 | func doThing() async throws { 235 | try await Task.sleep(nanoseconds: 1_000_000_000) 236 | } 237 | 238 | @main 239 | struct ProgramEntry { 240 | static func main() async throws { 241 | try await doThing() 242 | 243 | print("finished") 244 | } 245 | } 246 | ``` 247 | -------------------------------------------------------------------------------- /Recipes/Structured.md: -------------------------------------------------------------------------------- 1 | # Structured Concurrency 2 | 3 | Once you are in an async context, you can make use of structured concurrency. 4 | 5 | ## Lazy Async Value 6 | 7 | You'd like to lazily compute an async value and cache the result. 8 | 9 | ### Anti-Solution: Ignoring actor reentrancy 10 | 11 | I wanted to post this just to specifically highlight that this does not work correctly, but could be tempting. 12 | 13 | ```swift 14 | actor MyActor { 15 | private var expensiveValue: Int? 16 | 17 | private func makeValue() async -> Int { 18 | 0 // we'll pretend this is really expensive 19 | } 20 | 21 | public var value: Int { 22 | get async { 23 | if let value = expensiveValue { 24 | return value 25 | } 26 | 27 | // hazard 1: Actor Reentrancy 28 | let value = await makeValue() 29 | 30 | self.expensiveValue = value 31 | 32 | return value 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | ### Solution #1: Use an unstructured Task 39 | 40 | Move the async calculation into a Task, and cache that instead. This is straightforward, but because it uses unstructured concurrency, it no longer supports priority propagation or cancellation. 41 | 42 | ```swift 43 | actor MyActor { 44 | private var expensiveValueTask: Task? 45 | 46 | private func makeValue() async -> Int { 47 | 0 // we'll pretend this is really expensive 48 | } 49 | 50 | public var value: Int { 51 | get async { 52 | // note there are no awaits between the read and write of expensiveValueTask 53 | if let task = expensiveValueTask { 54 | return await task.value 55 | } 56 | 57 | let task = Task { await makeValue() } 58 | 59 | self.expensiveValueTask = task 60 | 61 | return await task.value 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ### Solution #2: Track Continuations 68 | 69 | Staying in the the structured concurrency world is more complex, but supports priority propagation. 70 | 71 | It is **essential** you understand the special properties of `withCheckedContinuation` that make this technique possible. That function is annotated with `@_unsafeInheritExecutor`. This provides the guarantee we need that **despite** the `await`, there will never be a suspension before the closure's body is executed. This is why we can safely access and mutate the `pendingContinuations` array without risk of races due to reentrancy. 72 | 73 | ```swift 74 | actor MyActor { 75 | typealias ValueContinuation = CheckedContinuation 76 | 77 | enum State { 78 | case empty 79 | case pending([ValueContinuation]) 80 | case filled(Int) 81 | } 82 | 83 | private var state = State.empty 84 | 85 | private func makeValue() async -> Int { 86 | 0 // we'll pretend this is really expensive 87 | } 88 | 89 | private func fill(with result: Int) { 90 | guard case let .pending(array) = state else { fatalError() } 91 | 92 | for continuation in array { 93 | continuation.resume(returning: result) 94 | } 95 | 96 | self.state = .filled(result) 97 | } 98 | 99 | private func trackContinuation(_ continuation: ValueContinuation) { 100 | guard case var .pending(array) = state else { fatalError() } 101 | 102 | array.append(continuation) 103 | 104 | self.state = .pending(array) 105 | } 106 | 107 | public var value: Int { 108 | get async { 109 | switch state { 110 | case let .filled(value): 111 | return value 112 | case .pending: 113 | // we have no value (yet!), but we do have waiters. When this continuation is finally resumed, it will have the value we produce 114 | 115 | return await withCheckedContinuation { continuation in 116 | trackContinuation(continuation) 117 | } 118 | case .empty: 119 | // this is the first request with no other pending continuations. It's critical that we make a synchronous state transition to ensure new callers are routed correctly. 120 | self.state = .pending([]) 121 | 122 | let value = await makeValue() 123 | 124 | fill(with: value) 125 | 126 | return value 127 | } 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | ### Solution #3: Track Continuations with Cancellation Support 134 | 135 | Here is a generic cache solution with also supports cancellation. It's complicated! It uses the `withTaskCancellationHandler`, which has similar properties to `withCheckedContinuation`. 136 | 137 | It is important to note that this implementation makes the very first request special, as it is actually filling in the cache. Cancelling the first request will have the effect of cancelling all other requests. 138 | 139 | ```swift 140 | actor AsyncCache where Value: Sendable { 141 | typealias ValueContinuation = CheckedContinuation 142 | typealias ValueResult = Result 143 | 144 | enum State { 145 | case empty 146 | case pending([UUID: ValueContinuation]) 147 | case filled(ValueResult) 148 | } 149 | 150 | private var state = State.empty 151 | private let valueProvider: () async throws -> Value 152 | 153 | init(valueProvider: @escaping () async throws -> Value) { 154 | self.valueProvider = valueProvider 155 | } 156 | 157 | private func makeValue() async throws -> Value { 158 | try await valueProvider() 159 | } 160 | 161 | private func cancelContinuation(with id: UUID) { 162 | guard case var .pending(dictionary) = state else { return } 163 | 164 | dictionary[id]?.resume(throwing: CancellationError()) 165 | dictionary[id] = nil 166 | 167 | self.state = .pending(dictionary) 168 | } 169 | 170 | private func fill(with result: ValueResult) { 171 | guard case let .pending(dictionary) = state else { fatalError() } 172 | 173 | for continuation in dictionary.values { 174 | continuation.resume(with: result) 175 | } 176 | 177 | self.state = .filled(result) 178 | } 179 | 180 | private func trackContinuation(_ continuation: ValueContinuation, with id: UUID) { 181 | guard case var .pending(dictionary) = state else { fatalError() } 182 | 183 | precondition(dictionary[id] == nil) 184 | dictionary[id] = continuation 185 | 186 | self.state = .pending(dictionary) 187 | } 188 | 189 | public var value: Value { 190 | get async throws { 191 | switch state { 192 | case let .filled(value): 193 | return try value.get() 194 | case .pending: 195 | // we have no value (yet!), but we do have waiters. When this continuation is finally resumed, it will have the value we produce 196 | 197 | let id = UUID() // produce a unique identifier for this particular request 198 | 199 | return try await withTaskCancellationHandler { 200 | try await withCheckedThrowingContinuation { continuation in 201 | // this has to go through a function call because the compiler seems unhappy if I reference pendingContinuations directly 202 | trackContinuation(continuation, with: id) 203 | } 204 | } onCancel: { 205 | // we must move back to this actor here to access its state, referencing the unique continuation id 206 | Task { await self.cancelContinuation(with: id) } 207 | } 208 | case .empty: 209 | // this is the first request with no other pending continuations. It's critical that we make a synchronous state transition to ensure new callers are routed correctly. 210 | self.state = .pending([:]) 211 | 212 | do { 213 | let value = try await makeValue() 214 | 215 | fill(with: .success(value)) 216 | 217 | return value 218 | } catch { 219 | fill(with: .failure(error)) 220 | 221 | throw error 222 | } 223 | } 224 | } 225 | } 226 | } 227 | ``` 228 | 229 | ### Solution #4: Hybrid Approach 230 | 231 | This is a hybrid structured/unstructured implemenation. It ensures that the cancellation behavior is the same across all requests, even the first. The downside to this is priority propagation will not work for the initial cache fill. This means that should a higher priority task end up needing the result, its priority will not be boosted. 232 | 233 | ```swift 234 | actor AsyncCache where Value: Sendable { 235 | typealias ValueContinuation = CheckedContinuation 236 | typealias ValueResult = Result 237 | 238 | enum State { 239 | case empty 240 | case pending(Task, [UUID: ValueContinuation]) 241 | case filled(ValueResult) 242 | } 243 | 244 | private var state = State.empty 245 | private let valueProvider: () async throws -> Value 246 | private let fillOnCancel: Bool 247 | 248 | init(fillOnCancel: Bool = true, valueProvider: @escaping () async throws -> Value) { 249 | self.valueProvider = valueProvider 250 | self.fillOnCancel = fillOnCancel 251 | } 252 | 253 | private func makeValue() async throws -> Value { 254 | try await valueProvider() 255 | } 256 | 257 | private func cancelContinuation(with id: UUID) { 258 | guard case .pending(let task, var dictionary) = state else { return } 259 | 260 | dictionary[id]?.resume(throwing: CancellationError()) 261 | dictionary[id] = nil 262 | 263 | guard dictionary.isEmpty else { 264 | self.state = .pending(task, dictionary) 265 | return 266 | } 267 | 268 | // if we have cancelled all continuations, should we cancel the underlying work? 269 | if fillOnCancel == false { 270 | task.cancel() 271 | self.state = .empty 272 | return 273 | } 274 | } 275 | 276 | private func fill(with result: ValueResult) { 277 | switch state { 278 | case .empty: 279 | // This should only occur if we have cancelled and do not want to fill the cache regardless. This is technically wasted work, but may be desirable for some uses. 280 | precondition(fillOnCancel == false) 281 | case let .pending(_, dictionary): 282 | for continuation in dictionary.values { 283 | continuation.resume(with: result) 284 | } 285 | 286 | self.state = .filled(result) 287 | case .filled: 288 | fatalError() 289 | } 290 | } 291 | 292 | private func trackContinuation(_ continuation: ValueContinuation, with id: UUID) { 293 | guard case .pending(let task, var dictionary) = state else { fatalError() } 294 | 295 | precondition(dictionary[id] == nil) 296 | dictionary[id] = continuation 297 | 298 | self.state = .pending(task, dictionary) 299 | } 300 | 301 | public var value: Value { 302 | get async throws { 303 | switch state { 304 | case let .filled(value): 305 | return try value.get() 306 | case .pending: 307 | break 308 | case .empty: 309 | // Begin an unstructured task to fill the cache. This gives all requests including first the same cancellation semantics. 310 | let task = Task { 311 | do { 312 | let value = try await makeValue() 313 | 314 | fill(with: .success(value)) 315 | } catch { 316 | fill(with: .failure(error)) 317 | } 318 | } 319 | 320 | // this is the first request with no other pending continuations. It's critical that we make a synchronous state transition to ensure new callers are routed correctly. 321 | self.state = .pending(task, [:]) 322 | } 323 | 324 | let id = UUID() // produce a unique identifier for this request 325 | 326 | return try await withTaskCancellationHandler { 327 | try await withCheckedThrowingContinuation { continuation in 328 | // this has to go through a function call because the compiler seems unhappy if I reference pendingContinuations directly 329 | trackContinuation(continuation, with: id) 330 | } 331 | } onCancel: { 332 | // we must move back to this actor here to access its state, referencing the unique continuation id 333 | Task { await self.cancelContinuation(with: id) } 334 | } 335 | } 336 | } 337 | } 338 | ``` -------------------------------------------------------------------------------- /Recipes/Protocols.md: -------------------------------------------------------------------------------- 1 | # Protocols 2 | 3 | Protocols are widely used in Swift APIs, but can present unique challenges with concurrency. 4 | 5 | ## Non-isolated Protocol 6 | 7 | You have a non-isolated protocol, but need to add conformance to an MainActor-isolated type. 8 | 9 | ```swift 10 | protocol MyProtocol { 11 | func doThing(argument: ArgumentType) -> ResultType 12 | } 13 | 14 | @MainActor 15 | class MyClass { 16 | } 17 | 18 | extension MyClass: MyProtocol { 19 | func doThing(argument: ArgumentType) -> ResultType { 20 | } 21 | } 22 | ``` 23 | 24 | ### Solution #1: non-isolated conformance 25 | 26 | ```swift 27 | extension MyClass: MyProtocol { 28 | nonisolated func doThing(argument: ArgumentType) -> ResultType { 29 | // at this point, you likely need to interact with self, so you must satisfy the compiler 30 | // hazard 1: Availability 31 | MainActor.assumeIsolated { 32 | // here you can safely access `self` 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | ### Solution #2: make the protocol async 39 | 40 | If the protocol is under your control, you can make it compatible by making all functions async. 41 | 42 | ```swift 43 | protocol MyProtocol { 44 | // hazard 1: Async Virality (for other conformances) 45 | func doThing(argument: String) async -> Int 46 | } 47 | 48 | @MainActor 49 | class MyClass: MyProtocol { 50 | // hazard 2: Async Virality (for callers) 51 | func doThing(argument: String) async -> Int { 52 | return 42 53 | } 54 | } 55 | ``` 56 | 57 | ### Solution #3: non-isolated actor conformance 58 | 59 | You can also use a variant of solution #1 to add conformance to an non-Main actor. This does come with limitations. Particular caution should be used if this protocol comes from Objective-C. It is common for Objective-C code to be incorrectly or insufficiently annotated, and that can result in violations of the Swift concurrency invariants. In other words, deadlocks and isolation failure (which will produce crashes, you **hope**). 60 | 61 | ```swift 62 | protocol NotAsyncFriendly { 63 | func informational() 64 | 65 | func takesSendableArguments(_ value: Int) 66 | 67 | func takesNonSendableArguments(_ value: NonSendableType) 68 | 69 | func expectsReturnValues() -> Int 70 | } 71 | 72 | actor MyActor { 73 | func doIsolatedThings(with value: Int) { 74 | ... 75 | } 76 | } 77 | 78 | extension MyActor: NotAsyncFriendly { 79 | // purely informational calls are the easiest 80 | nonisolated func informational() { 81 | // hazard: ordering 82 | // the order in which these informational messages are delivered may be important, but is now lost 83 | Task { 84 | await self.doIsolatedThings(with: 0) 85 | } 86 | } 87 | 88 | nonisolated func takesSendableArguments(_ value: Int) { 89 | // hazard: ordering 90 | Task { 91 | // we can safely capture and/or make use of value here because it is Sendable 92 | await self.doIsolatedThings(with: value) 93 | } 94 | } 95 | 96 | nonisolated func takesNonSendableArguments(_ value: NonSendableType) { 97 | // hazard: sendability 98 | 99 | // any action taken would have to either not need the actor's isolated state or `value`. 100 | // That's possible, but starting to get tricky. 101 | } 102 | 103 | nonisolated func expectsReturnValues() -> Int { 104 | // hazard: sendability 105 | 106 | // Because this function expects a synchronous return value, the actor's isolated state 107 | // must not be needed to generate the return. This is also possible, but is another indication 108 | // that an actor might be the wrong option. 109 | 110 | return 42 111 | } 112 | } 113 | ``` 114 | 115 | ### Solution #4: don't use a protocol 116 | 117 | This is not a joke! If you are in control of both the protocol and all of its usage, functions may be able to serve the same purpose. A good example of this is a delegate pattern. 118 | 119 | ```swift 120 | // example protocol/consumer pair 121 | protocol MyClassDelegate: AnyObject { 122 | func doThings() 123 | } 124 | 125 | class MyClass { 126 | weak var delegate: MyClassDelegate? 127 | } 128 | 129 | // problematic usage: 130 | @MainActor 131 | class MyUsage { 132 | let myClass: MyClass 133 | 134 | init() { 135 | self.myClass = MyClass() 136 | 137 | myClass.delegate = self 138 | } 139 | } 140 | 141 | extension MyUsage: MyClassDelegate { 142 | // this needs to be non-isolated 143 | nonisolated func doThings() { 144 | } 145 | } 146 | ``` 147 | 148 | Contrast this with a function-based implementation. This provides very flexible isolation - can work with a `MainActor` type but also a plain actor. 149 | 150 | ```swift 151 | class MyClass { 152 | var doThings: () -> Void = {} 153 | } 154 | 155 | actor MyUsage { 156 | let myClass: MyClass 157 | 158 | init() { 159 | self.myClass = MyClass() 160 | 161 | myClass.doThings = { [unowned self] in 162 | // accessing self is fine here because `doThings` is not Sendable 163 | print(self) 164 | } 165 | } 166 | } 167 | ``` 168 | 169 | ## Non-isolated NSObjectProtocol-inheriting Protocol 170 | 171 | This is similar to the above problem, but with the significant constraint that the type conforming to the protocol must inherit from `NSObject`. You can use all of the same solutions, with the exception of an actor type, which cannot inherit from anything. 172 | 173 | ```swift 174 | actor MyActor {} 175 | 176 | // ERROR: ... 'MyActor' should inherit 'NSObject' instead ... 177 | extension MyActor: URLSessionDelegate { 178 | } 179 | ``` 180 | 181 | ## Solution #1: Functional Proxy 182 | 183 | This solution works for objects that strongly retain their delegates. `URLSession` does, but I'm sure there are many examples types that do not. 184 | 185 | ```swift 186 | // first, make a simple type that conforms to the NSObjet-based protocol 187 | final class URLSessionDelegateProxy: NSObject { 188 | var eventsFinished: () -> Void = { } 189 | } 190 | 191 | extension URLSessionDelegateProxy: URLSessionDelegate { 192 | func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { 193 | eventsFinished() 194 | } 195 | } 196 | 197 | // Use the proxy as a stand-in 198 | actor MyActor { 199 | private let session: URLSession 200 | 201 | init() { 202 | let proxy = URLSessionDelegateProxy() 203 | 204 | self.session = URLSession(configuration: .default, delegate: proxy, delegateQueue: nil) 205 | 206 | // once self has been fully, initialized, assign the callbacks 207 | proxy.eventsFinished = { 208 | Task { await self.eventsFinished() } 209 | } 210 | } 211 | 212 | private func eventsFinished() { 213 | } 214 | } 215 | ``` 216 | 217 | ## Solution #2: Event Sequence 218 | 219 | Solution #1 doesn't work any more for `URLSessionDelegate`, because it now requires a `Sendable` type. If you do not have to return values any values, this can work. 220 | 221 | ```swift 222 | final class URLSessionDelegateProxy: NSObject { 223 | enum Event: Sendable { 224 | case didFinishEvents 225 | } 226 | 227 | private let streamPair = AsyncStream.makeStream() 228 | 229 | public var eventStream: AsyncStream { 230 | streamPair.0 231 | } 232 | } 233 | 234 | extension URLSessionDelegateProxy: URLSessionDelegate { 235 | func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { 236 | streamPair.1.yield(.didFinishEvents) 237 | } 238 | } 239 | 240 | // Use the proxy as a stand-in 241 | actor MyActor { 242 | private let session: URLSession 243 | 244 | init() { 245 | let proxy = URLSessionDelegateProxy() 246 | 247 | self.session = URLSession(configuration: .default, delegate: proxy, delegateQueue: nil) 248 | 249 | // once self has been fully, initialized, consume the events 250 | Task { [weak self] in 251 | for await event in proxy.eventStream { 252 | // don't forget to be careful with self's lifetime here 253 | guard let self else { break } 254 | 255 | switch event { 256 | case .didFinishEvents: 257 | await self.eventsFinished() 258 | } 259 | } 260 | } 261 | } 262 | 263 | private func eventsFinished() { 264 | } 265 | } 266 | ``` 267 | 268 | ## Non-isolated Init Requirement 269 | 270 | You need to satisfy an initializer requirement in a non-isolated protocol with an isolated type. This is a particularly tricky special-case of the problem above if you need to initialize instance variables. 271 | 272 | All credit to [Holly Borla](https://forums.swift.org/t/complete-checking-with-an-incorrectly-annotated-init-conformance/69955/6) for the solutions here. 273 | 274 | ```swift 275 | protocol MyProtocol { 276 | init() 277 | } 278 | 279 | @MainActor 280 | class MyClass: MyProtocol { 281 | let value: NonSendable 282 | 283 | // WARNING: Main actor-isolated initializer 'init()' cannot be used to satisfy nonisolated protocol requirement 284 | init() { 285 | self.value = NonSendable() 286 | } 287 | } 288 | ``` 289 | 290 | ### Solution #1: non-isolated conformance + SE-0414 291 | 292 | The solution below **will** work once [Region-Based Isolation](https://github.com/apple/swift-evolution/blob/main/proposals/0414-region-based-isolation.md) is available. But, right now it will still produce a warning. 293 | 294 | ```swift 295 | @MainActor 296 | class MyClass: MyProtocol { 297 | let value: NonSendable 298 | 299 | required nonisolated init() { 300 | // WARNING: Main actor-isolated property 'value' can not be mutated from a non-isolated context 301 | self.value = NonSendable() 302 | } 303 | } 304 | ``` 305 | 306 | ### Solution #2: unchecked Sendable transfer 307 | 308 | This solution resorts to an `@unchecked Sendable` wrapper to transfer the value across domains. You can see why Region-Based Isolation is so nice, as this solution is pretty ugly. 309 | 310 | ```swift 311 | struct Transferred: @unchecked Sendable { 312 | let value: T 313 | } 314 | 315 | @MainActor 316 | class MyClass: MyProtocol { 317 | private let _value: Transferred 318 | var value: NonSendable { _value.value } 319 | 320 | required nonisolated init() { 321 | self._value = Transferred(value: NonSendable()) 322 | } 323 | } 324 | ``` 325 | 326 | ### Solution #3: property wrapper 327 | 328 | This is a nicer and more-reusable version of #2. By setting up an `@unchecked Sendable` as a property wrapper you can reduce some of the boilerplate. 329 | 330 | ```swift 331 | @propertyWrapper 332 | struct InitializerTransferred: @unchecked Sendable { 333 | let wrappedValue: Value 334 | 335 | init(_ wrappedValue: Value) { 336 | self.wrappedValue = wrappedValue 337 | } 338 | } 339 | 340 | @MainActor 341 | class MyClass: MyProtocol { 342 | @InitializerTransferred private(set) var value: NonSendable 343 | 344 | required nonisolated init() { 345 | sself._value = InitializerTransferred(NonSendable()) 346 | } 347 | } 348 | ``` 349 | 350 | ## Static Variables 351 | 352 | You want to conform to a protocol that requires a **non-isolated** static let property. This can be really tricky to handle. This comes up a lot with SwiftUI's `EnvironmentKey` and `PreferenceKey`. 353 | 354 | ```swift 355 | // for reference, here's how EnvironmentKey is defined 356 | public protocol EnvironmentKey { 357 | associatedtype Value 358 | 359 | static var defaultValue: Self.Value { get } 360 | } 361 | 362 | class NonSendable { 363 | } 364 | 365 | struct MyKey: EnvironmentKey { 366 | // WARNING: Static property 'defaultValue' is not concurrency-safe because it is not 367 | // either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6 368 | static let defaultValue = NonSendable() 369 | } 370 | ``` 371 | 372 | ### Solution #1: MainActor + non-isolated init 373 | 374 | This solution took me a while to even fully understand. Adding `MainActor` establishes isolation, making the type Sendable. But, that also means the now `MainActor`-isolated init cannot be used at the definition site. Remember, `defaultValue` is non-isolated. If your type doesn't need to reference other `MainActor`-isolated types in its init, which is surprisingly common, this will work well. 375 | 376 | ```swift 377 | @MainActor 378 | class NonSendable { 379 | nonisolated init() { 380 | } 381 | } 382 | 383 | struct MyKey: EnvironmentKey { 384 | // This is now ok because: 385 | // a) our type is globally-isolated (and that means Sendable) 386 | // b) the init can be called from a non-isolated context 387 | static let defaultValue = NonSendable() 388 | } 389 | ``` 390 | 391 | ### Solution #2: MainActor + non-isolated init + default values 392 | 393 | A non-isolated `init` can be hard to create when the type involves non-Sendable properties. Sometimes you can work around this by using default property values. 394 | 395 | ```swift 396 | @MainActor 397 | class NonSendable { 398 | // this is MainActor-only 399 | private var value = AnotherNonSendable() 400 | 401 | nonisolated init() { 402 | // ok because self's isolated properties are not accessed 403 | } 404 | } 405 | ``` 406 | 407 | ### Solution #3: Define a read-only accessor 408 | 409 | Instead of trying to share a single non-Sendable value, create a new one on each access. This can work really well if the value is not accessed frequently and distinct instances make sense for your usage. 410 | 411 | ```swift 412 | struct MyKey: EnvironmentKey { 413 | static var defaultValue: NonSendable { NonSendable() } 414 | } 415 | ``` 416 | 417 | ### Solution #4: nonisolated(unsafe) Optional 418 | 419 | If you happen to be storing an optional value, you can safely cheat **if** you want to initialize the value to nil. But, you have to be careful here. It is very unusual for instances of a type to differ in thread-safety. This is just a very special case. 420 | 421 | ```swift 422 | struct MyKey: EnvironmentKey { 423 | nonisolated(unsafe) static let defaultValue: NonSendable? = nil 424 | } 425 | ``` 426 | --------------------------------------------------------------------------------