Swift Concurrency: Feedback Wanted!

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

8 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.

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: Second Teaser: Practical Multithreading – Wandering Coder ; 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

It’s probably easier to think of it in terms of what is running on the main actor rather than what isn’t. At the moment there’s only a few ways to execute something on an actor (which are really all the same).

  1. Call it with a method marked within a global actor (@MainActor).
  2. Call it when isolated within a particular actor instance.
  3. Enqueue it using Task {}, which inherits the current actor’s context.

Right now the easiest way is to mark any methods you wan on the main actor @MainActor and let the system take care of the rest.

1 Like

I ended up changing the design altogether, certainly for the better, but having to pay an additional allocation per object doesn‘t seem ideal. Still, not sure it is a valid enough use case to warrant the now removed nonisolated(unsafe), even when subclassing, e.g., UIKit classes.

1 Like

I’m not sure, but I think the answer is yes, you can assume that "default" actors and tasks (i.e. those that don't use @MainActor or other custom executors) run off the main thread.

The reason is that the default concurrent and serial executors use the cooperative thread pool, and the main thread is not part of that pool (I think).

1 Like

Also, I would not expect a task with lower than highest priority to run on the main thread, unless it's specifically assigned to the MainActor. Even if you start a task from the main thread it will not get the highest priority.

It seems like your multiplication should be division, no?

Terms of Service

Privacy Policy

Cookie Policy