Swift Concurrency: Feedback Wanted!

I think I love the structured concurrency and I've been having a good play (and have largely converted a personal app over already). That said in some experimentation I'm being stymied and I'm not sure if it is understanding (possibly documentation) gaps or bugs in current implementation.

I've been experimenting with creating my own simple AsyncSequences in ways that actually cut across a lot of the structured concurrency features. I know AsyncStream should make this simpler but I wanted to implement manually to see how it really worked (and I also couldn't find AsyncStream so maybe it isn't available yet).

[Edit: I think I've found the issue, something frequently goes wrong when an continuation is resumed from an actor context. I think I have a solution where the actor manages the continuation but returns it to be called from outside the actor instead of calling it directly itself. Raised as: SR-14875]

I think in particular I'm not seeing the reentrancy behaviour I expect. For example this doesn't work (or at least works only occasionally - maybe I'm missing something obvious):

AsyncTimerActorSequence
@available(macOS 12.0, iOS 15.0, *)
public struct AsyncTimerActorSequence : AsyncSequence {
    
    public typealias AsyncIterator = Iterator
    public typealias Element = Void
    
    let interval: TimeInterval
    
    public init(interval: TimeInterval) {
        self.interval = interval
    }
    
    public func makeAsyncIterator() -> Iterator {
        let itr = Iterator()
        Task {
            await itr.start(interval: interval)
        }
        return itr
    }
    
    public actor Iterator : AsyncIteratorProtocol {
        
        private var timer: Timer?
        private var continuation: CheckedContinuation<(), Never>?
        
        fileprivate init() {}
        
        fileprivate func start(interval: TimeInterval) {
            let t = Timer(fire: .now, interval: interval, repeats: true) { [weak self] _ in
                guard let s = self else { return }
                Task.detached {
                    await s.fireContinuation()
                }
            }
            timer = t
            RunLoop.main.add(t, forMode: .default)
        }
        
        private func fireContinuation()  {
            continuation?.resume()
            continuation = nil
        }
        
        public func next() async throws -> ()? {
            await withCheckedContinuation { continuation in
                self.continuation = continuation
            }
            return ()
        }
        
        deinit {
            timer?.invalidate()
        }
    }
}

When I use a class instead it seems to work well although I worry about races setting and firing the continuation which is why I leant on the actor. I thought the reentrancy at suspension points meant this would work.

It might be that there is some information that I'm so far missing, possibly in relation to limitations of reentrancy in Actors or the interaction between Actors and continuations.

"AsyncTimerSequence (class based iterator - works but I worry about races setting/getting/clearing continuation
@available(macOS 12.0, iOS 15.0, *)
public struct AsyncTimerSequence : AsyncSequence {
    
    public typealias AsyncIterator = Iterator
    public typealias Element = Void
    
    let interval: TimeInterval
    
    public init(interval: TimeInterval) {
        self.interval = interval
    }
    
    public func makeAsyncIterator() -> Iterator {
        Iterator(interval: interval)
    }
    
    public final class Iterator : AsyncIteratorProtocol {
        
        private var timer: Timer?
        private var continuation: CheckedContinuation<(), Never>?
        
        init(interval: TimeInterval) {
            let t = Timer(fire: .now, interval: interval, repeats: true) { [weak self] _ in
                if let continuation = self?.continuation {
                    // I worry about a race condition here but I think it is safe if we don't resume the continuation until it has been cleared.
                    self?.continuation = nil
                    continuation.resume()
                }
            }
            timer = t
            RunLoop.main.add(t, forMode: .default)
        }
        
        public func next() async throws -> ()? {
            await withCheckedContinuation { continuation in
                self.continuation = continuation
            }
        }
        
        deinit {
            timer?.invalidate()
        }
    }
}

Note that these versions are only robust to the situation where next() is not called multiple times concurrently (I have a version with an array as a queue - need to look up the state of dequeue) but I don't think I've seen anything in the documentation disallowing concurrent calls to next(). I think it is easy to write sequences which don't handle that properly and it may be a source of bugs. Clearly in the normal for try await let foo in sequence { you won't get concurrent calls but if people start unrolling or async leting a number of items I can see it happening.

An additional albeit temporary complication is differences between builds currently available as the features and syntax evolves and also various compiler flags that affect the concurrency behaviour. Are the 5.5 snapshots the right ones to be using or should I be using the trunk snapshots or whichever happens to be the newest build on a given day? Is there a good way to track what the changes are between snapshots or compared with Xcode releases?

It would be nice, if Task.sleep would also accept a non-negative TimeIntervall. Personally, nanoseconds in UInt64 are hard to read.

let duration: TimeIntervall = 2.0
await Task.sleep(duration)
// vs
await Task.sleep(2 * 1_000_000_000)  // Two seconds

Or, with this extension functions:

private extension TimeInterval {
    var seconds: TimeInterval {
        self
    }
    var minutes: TimeInterval {
        seconds * 60
    }
    var hours: TimeInterval {
        minutes * 60
    }
    var days: TimeInterval {
        hours * 24
    }
}

private extension Int {
    var seconds: TimeInterval {
        Double(self)
    }
    var minutes: TimeInterval {
        seconds * 60.0
    }
    var hours: TimeInterval {
        minutes * 60.0
    }
    var days: TimeInterval {
        hours * 24.0
    }
}

await Task.sleep(2.seconds)

In Kotlin, a similar function exists

delay(2.seconds)

// with
suspend fun delay(duration: Duration)
val Int.seconds get() = Duration.seconds(this)
2 Likes

Thanks for the feedback, we aimed to provide such nice “duration” based api but had to defer it out of this release because we want to holistically address the general topic of time and deadline APIs together. Hope we’ll get back to it soon enough :slight_smile: Once we have a duration and deadline types we can offer a new sleep that would take such arguments :+1:

11 Likes

(Also note that days are not uniform lengths, typically they range from 23 to 25 hours long. From past experience, designing duration related APIs that correctly traffic in days is nontrivial and produces results that surprise users of the API)

10 Likes

Quick question: If Actor types do not support inheritance, then why is final actor legal?

Right now, final still has an ABI impact, where it means "something that will never ever be subclassed or overridden". The Core Team guidance on actor inheritance is that it is not part of the proposal, but they/we did not shut the door on it completely. Allowing final lets us keep the door open in the ABI and language to change our minds if actor inheritance turns out to be very important, and we can ban it later on if we decide that Swift will never have actor inheritance.

Doug

11 Likes

Question about Async Sequence. Is it valid to await multiple calls to next() concurrently? It appears from current code that it isn't allowed as the iterator would be captured in the async let so it seems that it isn't possible to async let from an AsyncSequence. Is that correct?

If it is allowed it will have design implications on AsyncSequence implementations. If it isn't allowed then it may impede some potential unrolled loop approaches and there probably should be clear guidance on it if not alerts and warnings.

Example of how it could be useful if it was possible (although probably not worth the overhead in the implementation of AsyncSequences):

var array = [UInt16]()
var iterator = myByteAsyncSequence.makeAsyncIterator()
while true {
  async let bottomByte = try await iterator.next()
  async let topByte = try await iterator.next()
  let val = await UInt16(bottomByte ?? 0) + (UInt16(topByte ?? 0) << 8)
  array.append(val)
  guard  topByte != nil else { break }
}
2 Likes

Makes sense! But then, wouldn’t it make sense to force the usage of final? This way, if and when actor inheritance becomes a thing, it win’t be a breaking change, whereas without final, an implicit inheritance potential would appear out of nowhere and the migrator would need to add final on every actor to preserve the semantics. On the other hand, if Swift never gains actor inheritance, then the final would be nonsensical. On the day when actor inheritance becomes forever illegal, the final actor would become deprecated in favor of simply actor. In both cases, forcing the usage of final seems to lead to least amount of headache. For now, i just spell out final explicitly for the sake of clarity and consistency with classes. What do you think?

Per the discussion on SE-0306, the expectation is that actor inheritance will not come back. Forcing everyone to write final on every actor is inconsistent with that direction. Allowing final lets folks who have ABI considerations (which is a tiny, tiny slice of the Swift ecosystem) lock down their actor ABI for actor types they know will never be subclassable no matter what we do in the language. For everyone else, it's irrelevant: actor inheritance itself is not permitted, and if it came back, no code outside of the actor's defining module can be impacted because open is not permitted.

Doug

2 Likes

Makes total sense! Thank you very much for your time!

I … think I've come up with a situation where actor inheritance would be beneficial. I've written a series of blog posts that describe an approach to a networking framework, where the fundamental "type" of networking middleware is this class interface:

open class HTTPLoader {
    public private(set) var nextLoader: HTTPLoader?

    public init()

    public final func setNextLoader(_ next: HTTPLoader)

    open func load(task: HTTPTask) // task = request + response handler
    open func reset(with group: DispatchGroup)    
}

There is some logic in the base implementation of HTTPLoader that does things like detecting cycles in the nextLoader chain. I also have other subclasses that abstract certain kinds of algorithms (such as "modifying a request before passing it on to the next loader". In general the "last" loader in the chain is one that wraps a URLSession (or a mock session when unit testing).

There's a lot about this class that makes it behave like an actor, and I've thought that making it one could simplify a lot of the threadsafety logic I've got in some of my subclasses. (Many of these subclasses keep a cache of "in-flight" requests so they can perform pre- or post-execution logic, as dictated by the request itself, and making that thread safe has been a pain).

So, I think this is a situation where I'd want to change this to be an open actor HTTPLoader and then allow custom "subclasses" of it for different request behavior: retrying failed requests, modifying request headers, caching results, logging traffic, throttling the number of current requests, timing out requests, authenticating requests, etc. I've also got a couple of loaders that wrap multiple loaders, such as a general "Authentication" loader that wraps an OAuth + Basic Auth + Custom Auth loader, and then picks the right one based on what the request specifies.

Does that sound like a good use of actor subclassing, or is there another approach to this in an actor world?

Actor inheritance is generally useful but no single use case seems likely to reverse the decision made in 306. Any type which needs polymorphism with shared state and implementation and needs thread-safety would benefit.

Replacements I've explored to some degree so far.

  • Actors with protocol polymorphism and shared state encapsulated in another actor. Works, but the maintenance of the protocol is a major pain, and without shared executors talking to the internal actor is more inefficient than it needs to be.
  • Classes with subclass polymorphism using actors internally for thread safety. Impedance in the async work makes this tough to use, even if you get the polymorphism for free.
  • Complete protocol abstraction, sharing state definitions and common extension functionality. Even worse protocol maintenance and it's impossible to override and call the original implementation cleanly.

To my mind, there's no good replacement here, and I've never received a response about what the intended replacement is when I've asked.

1 Like

in otherwords, something like this? ↓

protocol Engine: Actor {}
actor Deisel: Engine {}
actor Normal: Engine {}

actor Automobile<Engine> {}

typealias Truck = Automobile<Deisel>
typealias Car = Automobile<Normal>

i could see it getting complicated if the car had more than 1 "parts" i guess...

I am adding @MainActor to my view model (an ObservableObject) in a SwiftUI application.

There is a small annoyance. I have (lot of) code which looks like this:

Button(action: model.someFunction) {
     Text("someFunction Button")
}

After adding @MainActor to the ObservableObject, this fails to compile with error Converting function value of type '@MainActor () -> ()' to '() -> Void' loses global actor 'MainActor'.

It has to be changed as follows to compile:

Button {
    model.someFunction()
}
label: {
    Text("someFunction Button")
}

Wondering if anything can be done that this still works.

3 Likes

Thanks for the feedback topic. I've been working on this code for some time, based on the proposals as they came (with the Swift nightly snapshots), and I was hoping I'd be able to actually get to a compiled binary using Xcode 13 beta and Swift 5.5; unfortunately, at this point the compilation never completes, no matter how long I wait (needless to say, I did fix any error or warning that previously came up).

//
//  Created by Pierre Lebeaupin on 17/01/2021.
//  Copyright © 2021 Pierre Lebeaupin. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

import Foundation;

enum Op: String {
    case additive = "+", multiplicative = "*";
    
    func neutralValue() -> UInt32 {
        return ((self == .additive) ? 0 : 1);
    }
    
    func combine(_ a: UInt32, _ b: UInt32) -> UInt32 {
        return ((self == .additive) ? (a + b) : (a * b));
    }

    func uncombine(_ a: UInt32, _ b: UInt32) -> UInt32 {
        return ((self == .additive) ? (a - b) : (a / b));
    }
}


struct ReadyValue {
    let value: UInt32;
    let op: Op?;
    let description: () -> String;
    // Note that as a closure, it can depend on more than the stored properties, contrary to a
    // computed property.

    init(_ inValue: UInt32) {
        value = inValue;
        op = nil;
        description = { return inValue.description;};
    }
    
    init(value inValue: UInt32, op inOp: Op, description indescription: @escaping () -> String) {
        value = inValue;
        op = inOp;
        description = indescription;
    }
}

struct DispatchedFunctor {
    typealias Dispatcher = (_ l: inout [ReadyValue],
                        _ composedCount: inout Int,
                        _ otherComposedCount: inout Int,
                        _ decomposedValue: inout [Bool:UInt32],
                        _ kind: Op,
                        _ startingFrom: Int,
                        _ walkOperands: @escaping (_ action: (_ value: ReadyValue, _ reverse: Bool) -> Void?) -> Void?,
                        _ dispatched: DispatchedFunctor) async -> Void;

    let apply: (_ l: inout [ReadyValue],
                        _ composedCount: inout Int,
                        _ otherComposedCount: inout Int,
                        _ decomposedValue: inout [Bool:UInt32],
                        _ kind: Op,
                        _ startingFrom: Int,
                        _ walkOperands: @escaping (_ action: (_ value: ReadyValue, _ reverse: Bool) -> Void?) -> Void?,
                        _ dispatcher: Dispatcher) async -> Void;
}

func resolveCore(_ injectedDispatcher: DispatchedFunctor.Dispatcher, _ resultReceiver: @escaping (_ result: String) -> Void, _ startGoal: UInt32, _ primitives: [UInt32]) async {
    //        await { () async -> Void in
    var referenceL = [ReadyValue]();
    var referenceComposedCount = 0;
    var referenceOtherComposedCount = 0;
            
            
    for element in primitives {
        referenceL.append(ReadyValue(element));
    }
        
    await exploreAdditionOfRightNode(&referenceL,
                                     &referenceComposedCount,
                                     &referenceOtherComposedCount,
                                     injectedDispatcher,
                                     kind: .additive);
    await exploreAdditionOfRightNode(&referenceL,
                                     &referenceComposedCount,
                                     &referenceOtherComposedCount,
                                     injectedDispatcher,
                                     kind: .multiplicative);
    //        }(); // https://bugs.swift.org/browse/SR-12243
            
            
    @Sendable func exploreAdditionOfRightNode(_ l: inout [ReadyValue],
                                              _ composedCount: inout Int,
                                              _ otherComposedCount: inout Int,
                                              _ dispatcher: DispatchedFunctor.Dispatcher,
                                              kind: Op) async {
        if (l.count != 0) && (l[l.count - 1].op != kind) {
            guard otherComposedCount == 0 else {
                return;
            }
                    
            otherComposedCount = composedCount;
            composedCount = 0;
        }
        defer {
            if (l.count != 0) && (l[l.count - 1].op != kind) {
                composedCount = otherComposedCount;
                otherComposedCount = 0;
            }
        }
                
        var referenceDecomposedValue = [false: kind.neutralValue(), true: kind.neutralValue()];
                
        await dispatcher(&l,
                         &composedCount,
                         &otherComposedCount,
                         &referenceDecomposedValue,
                         kind,
                         /* startingFrom: */ 0,
                         { _ in return nil;},
                         DispatchedFunctor(apply: iteratePossibleLeftNodes));
    }
            
    @Sendable func iteratePossibleLeftNodes(_ l: inout [ReadyValue],
                                            _ composedCount: inout Int,
                                            _ otherComposedCount: inout Int,
                                            _ decomposedValue: inout [Bool:UInt32],
                                            _ kind: Op,
                                            _ startingFrom: Int,
                                            _ walkOperands: @escaping (_ action: (_ value: ReadyValue, _ reverse: Bool) -> Void?) -> Void?,
                                            _ dispatcher: DispatchedFunctor.Dispatcher) async {
        for candidateOffset in startingFrom ..< (l.count - composedCount) {
            let rightChild = l.remove(at: candidateOffset);
            if let _ = rightChild.op {
                otherComposedCount -= 1;
            }
            defer {
                if let _ = rightChild.op {
                    otherComposedCount += 1;
                }
                l.insert(rightChild, at: candidateOffset);
            }
                    
            for phase in 0...1 {
                let reverse = (phase == 1);
                { (_ valueComponent: inout UInt32) in
                    valueComponent = kind.combine(valueComponent, rightChild.value);
                }(&decomposedValue[reverse]!);
                defer {
                    { (_ valueComponent: inout UInt32) in
                        valueComponent = kind.uncombine(valueComponent, rightChild.value);
                    }(&decomposedValue[reverse]!);
                }
                        
                let selfNode = {(_ action: (_ value: ReadyValue, _ reverse: Bool) -> Void?) -> Void? in
                    return action(rightChild, reverse) ?? walkOperands(action);
                };
                        
                await dispatcher(&l,
                                 &composedCount,
                                 &otherComposedCount,
                                 &decomposedValue,
                                 kind,
                                 /* startingFrom: */ candidateOffset,
                                 selfNode,
                                 DispatchedFunctor(apply: iteratePossibleLeftNodes));
                        
                // close current composition
                var num = 0;
                guard (selfNode({_,_ in guard num == 0 else {return ();}; num += 1; return nil;}) != nil)
                        && ( (kind == .additive) ? decomposedValue[false]! > decomposedValue[true]! :
                                ((decomposedValue[false]! % decomposedValue[true]!) == 0) ) else {
                            continue;
                        }
                        
                let realizedValue = kind.uncombine(decomposedValue[false]!, decomposedValue[true]!);
                let description = { () -> String in
                    var current = "(";
                    selfNode({(_ value: ReadyValue, _ freverse: Bool) -> Void? in
                        current += " ";
                        current += (freverse ? (kind == .additive ? "-" : "/") : kind.rawValue);
                        current += " ";
                        current += value.description();
                                
                        return nil;
                    });
                    current += ")";
                            
                    return current;
                };
                        
                guard l.count > 0 else {
                    if realizedValue == startGoal {
                        resultReceiver(description());
                    }
                    continue;
                }
                        
                composedCount += 1;
                l.append(ReadyValue(value: realizedValue, op: kind, description: description));
                defer {
                    l.remove(at: l.count - 1);
                    composedCount -= 1;
                }
                        
                await exploreAdditionOfRightNode(&l,
                                                 &composedCount,
                                                 &otherComposedCount,
                                                 dispatcher,
                                                 kind: .additive);
                await exploreAdditionOfRightNode(&l,
                                                 &composedCount,
                                                 &otherComposedCount,
                                                 dispatcher,
                                                 kind: .multiplicative);
            }
        }
    }
}


@Sendable func iteratePossibleLeftNodesFakeDispatch(_ l: inout [ReadyValue],
                                                    _ composedCount: inout Int,
                                                    _ otherComposedCount: inout Int,
                                                    _ decomposedValue: inout [Bool:UInt32],
                                                    _ kind: Op,
                                                    _ startingFrom: Int,
                                                    _ walkOperands: @escaping (_ action: (_ value: ReadyValue, _ reverse: Bool) -> Void?) -> Void?,
                                                    _ inner: DispatchedFunctor) async {
    await inner.apply(&l,
                      &composedCount,
                      &otherComposedCount,
                      &decomposedValue,
                      kind,
                      startingFrom,
                      walkOperands,
                      iteratePossibleLeftNodesFakeDispatch);
}
    
@available(macOS 12.0, *) protocol ResultReceiver: Actor {
    func receive(result: String);
}

@available(macOS 12.0, *) func resolveAsync(_ resultReceiver: ResultReceiver, _ startGoal: UInt32, _ primitives: UInt32...) async {
    await withTaskGroup(of: Void.self) {group in
        var outstandingTasks = UInt(0);
        
        func iteratePossibleLeftNodesDispatch(_ l: inout [ReadyValue],
                                              _ composedCount: inout Int,
                                              _ otherComposedCount: inout Int,
                                              _ decomposedValue: inout [Bool:UInt32],
                                              _ kind: Op,
                                              _ startingFrom: Int,
                                              _ walkOperands: @escaping (_ action: (_ value: ReadyValue, _ reverse: Bool) -> Void?) -> Void?,
                                              _ inner: DispatchedFunctor) async {
            let workloadEstimator = l.count + (walkOperands({_,_ in return ();}) != nil ? 1 : 0);
            /* Among other properties, this estimator value is monotonic. */
        
            // 6 may be too many to fit in a tick (1/60th of a second)
            // 4 already means too few possibilities to explore
            // 3 is right out
            if workloadEstimator == 5 {
                // reseat this divergence over a copy of the whole state
                let paramL = l;
                let paramComposedCount = composedCount;
                let paramOtherComposedCount = otherComposedCount;
                let paramDecomposedValue = decomposedValue;
            
                group.spawn {
                    var childL = paramL;
                    var childComposedCount = paramComposedCount;
                    var childOtherComposedCount = paramOtherComposedCount;
                    var childDecomposedValue = paramDecomposedValue;
                
                    await inner.apply(&childL,
                                      &childComposedCount,
                                      &childOtherComposedCount,
                                      &childDecomposedValue,
                                      kind,
                                      startingFrom,
                                      walkOperands,
                                      iteratePossibleLeftNodesFakeDispatch);
                }
                outstandingTasks += 1;
            
                while outstandingTasks >= ProcessInfo.processInfo.activeProcessorCount {
                    _ = await group.next();
                    outstandingTasks -= 1;
                }
            
                /* Note that as a result, the code will adapt in case activeProcessorCount changes.
                 At least, it will do so eventually, not with any sort of timeliness: for example,
                 if activeProcessorCount increases somehow, we will have to wait for one task to
                 complete before the code will work towards making outstandingTasks equal the new
                 value. */
            } else {
                await inner.apply(&l,
                                  &composedCount,
                                  &otherComposedCount,
                                  &decomposedValue,
                                  kind,
                                  startingFrom,
                                  walkOperands,
                                  iteratePossibleLeftNodesDispatch);
            }
        }
        
        await resolveCore(iteratePossibleLeftNodesDispatch, {result in
            async {
                await resultReceiver.receive(result: result);
            }
            }, startGoal, primitives);

        // outstanding tasks in the group are awaited at that point, according to the spec
        // https://github.com/DougGregor/swift-evolution/blob/structured-concurrency/proposals/nnnn-structured-concurrency.md
    };
}

(you ought to be able to try it out with, say, await resolveAsync(printerActor, 70, 5, 4, 3, 2, 2);)

This is derived from code that does work, using GCD instead: http://wanderingcoder.net/2021/01/13/second-teaser-practical-multithreading/ ; I can't locate any diagnostic info for this failure, is there anything you would like me to try to provide you some?

I get the same issue when using beta 3. For what it's worth, I sampled the swift-frontend process and it appears to be stuck in an infinite loop (or equivalent) in TopDownClosureFunctionOrder::visitFunctions(). Same for the SourceKitService process, by the way.

With regard to SE-0313—how would I go about having a stored struct property on a subclass of a @MainActor class that is accessible in nonisolated contexts because its type is, e.g., a Sendable unsafe lock-free queue?

I get nonisolated cannot be applied to stored properties.

AFAIK this isn't possible due to the removal of nonisolated(unsafe), which would allow developers to provide their own safety guarantees using things like locks.

Not very pretty but I presume you could wrap the struct in a private class instance (possibly private nested class) and expose it through nonisolated computed accessors.

How can I make sure that the newly created Task (or some computation) won’t run on the main thread? For example, let’s say, I have a CPU-bound task, and I want schedule it off the main thread.

I made a few experiments, and I see that:

  1. If I create an actor and await a computation on that actor, the computation is scheduled on the background thread. Is it always the case for non-main actors?
  2. If I launch a detached task with a background priority, it also goes off the main thread.

Therefore, I think, more broadly my question is: what are the relationship between Tasks, Actors and Threads? As far as I understand, I shouldn’t really think about the threads in this model, but it seems useful sometimes.

2 Likes