About Swift 6, Task Errors when trying to call a dependency object

Hi community,

Thank you for taking the time to read this.
Currently, I’m working on a sample project to better understand how to address various challenges involved in migrating from Swift 5 to Swift 6.

Now I’m encountering a challenge that I believe is quite common.

final class MarvelWorldRepository: MarvelWorldRepositoryInput {
    weak var output: (any MarvelWorldRepositoryOutput)? = nil

    private let marvelCharacterServiceInput: any MarvelCharacterServiceInput
    private let marvelBinaryDataSource: any MarvelBinaryDataSource

    init(
        marvelCharacterServiceInput: any MarvelCharacterServiceInput,
        marvelBinaryDataSource: any MarvelBinaryDataSource
    ) {
        self.marvelCharacterServiceInput = marvelCharacterServiceInput
        self.marvelBinaryDataSource = marvelBinaryDataSource
    }

   func getMarvelCharacter(_ identifier: String) {
        Task { [weak self] in
            guard let self else {
                return
            }

            do {
                let character = try await marvelCharacterServiceInput.getCharacter(identifier)
                output?.characterResponse(.success(character))
            } catch {
                print("Something went wrong \(error.localizedDescription)")
            }
        }
    }
}

And the compiler in swift 5 mode with complete concurrency enabled warns.

I have a couple of thoughts on this. First, I considered making the MarvelWorldRepository an actor, but that requires using await and involves an external task when calling the repository, which is what I wanted to avoid.

My second thought was to change the output variable to a let, which resolves the warning. However, that means you can't set it outside of the initialization, and of course, the reference cycle isn't broken due to the lack of a weak reference.

Any advice on this would be greatly appreciated.

2 Likes

I think the issue here is you've got a non-Sendable type that is participating in concurrency. So here's the question that comes up in my mind when I look at this code:

func getMarvelCharacter(_ identifier: String) {
  Task { [weak self] in
    // what thread/queue/actor do you think this should be executing on?
  }
}

And I think the answer to this really depends on what parts of your system need to read self.output. If that is read by a MainActor type, I think this should also be MainActor. What do you think?

1 Like

Thank you, Mattie!

Even doing this simple code, unfortunately, the compiler continues to issue warnings even with this approach.

Task { [weak self] in
    guard let self else {
          return
    }
}

My intention is for the task to run on a background thread rather than the main UI thread.
I appreciate any insights you may have on this!

You are welcome!

These warnings are still 100% expected, because we have not resolved the ambiguity on what is protecting the type and its output state. Does that need to be accessed from the main thread?

1 Like

No, the main thread won't be involved in that

Ok so you would like this type to be created and accessed exclusively in the background, is that right? Does the things that create/own these types have a queue or some other synchronization mechansism to protect them?

Sorry for continuing from another account.

No, the creator has no provide any mechanism. And yes, it only will be accessed in background

I'm always an advocate for less concurrency, because it is much simpler to reason about and build. So, I wouldn't go for an actor, especailly since you've already said you'd like to preserve synchronous access.

Your system has no synchronization, so I'm pretty sure you have a race today. But, I don't see enough of the system to comment meaningfully on a way to improve.

2 Likes

Thanks for your reply.

Just to make sure I understand correctly: In Swift 6, I can't use Tasks in this way because a Task requires sendable types. If you mark a type as sendable, you're responsible for implementing a mechanism to ensure mutual exclusion for that type, making sure any variables that aren't value types can't be accessed or modified by multiple threads at the same time.

There's no way to use async/await without adjusting the function signature to make it async, which means you’d also have to handle the same issue in the calling function.

The closer a Task is to the UI, the more non-value types you have to mark as Sendable, and the more mechanisms you need to implement.

The only other alternative is to skip async/await and revert to using closures, possibly with queues to manage thread changes and prevent overwhelming the UI thread.

So for the case we have, a possible approach could be:

final class MarvelWorldRepository: @unchecked Sendable, MarvelWorldRepositoryInput {
    @CustomGlobalActor weak var output: (any MarvelWorldRepositoryOutput)? = nil

    private let marvelCharacterServiceInput: any MarvelCharacterServiceInput
    private let marvelBinaryDataSource: any MarvelBinaryDataSource

    init(
        marvelCharacterServiceInput: any MarvelCharacterServiceInput,
        marvelBinaryDataSource: any MarvelBinaryDataSource
    ) {
        self.marvelCharacterServiceInput = marvelCharacterServiceInput
        self.marvelBinaryDataSource = marvelBinaryDataSource
    }

   func getMarvelCharacter(_ identifier: String) {
        Task { @CustomGlobalActor [weak self] in
            guard let self else {
                return
            }

            do {
                let character = try await marvelCharacterServiceInput.getCharacter(identifier)
                output?.characterResponse(.success(character))
            } catch {
                print("Something went wrong \(error.localizedDescription)")
            }
        }
    }
}

or using other mechanism like GCD:

final class MarvelWorldRepository: @unchecked Sendable, MarvelWorldRepositoryInput {
    private let marvelCharacterServiceInput: any MarvelCharacterServiceInput
    private let marvelBinaryDataSource: any MarvelBinaryDataSource

    private let outputQueue = DispatchQueue(label: "MarvelWorldRepositoryOutputQueue", qos: .background)

    private weak var output: (any MarvelWorldRepositoryOutput)?

    init(
        marvelCharacterServiceInput: any MarvelCharacterServiceInput,
        marvelBinaryDataSource: any MarvelBinaryDataSource,
        output: (any MarvelWorldRepositoryOutput)?
    ) {
        self.marvelCharacterServiceInput = marvelCharacterServiceInput
        self.marvelBinaryDataSource = marvelBinaryDataSource
        self.output = output
    }

    func getMarvelCharacter(_ identifier: String) {
        Task {
            do {
                let character = try await marvelCharacterServiceInput.getCharacter(identifier)
                outputQueue.async { [weak self] in
                    self?.output?.characterResponse(.success(character))
                }
            } catch {
                print("Something went wrong: \(error.localizedDescription)")
            }
        }
    }
}

Is that correct?

Thanks for your time.

Your respose here touches on a lot of concepts. I don't think I can cover everything, but I'm going to try give you a few general ideas.

In Swift 6, I can't use Tasks in this way because a Task requires sendable types.

Yes this is true. The closure passed into Task can only capture Sendable types. Many other systems aside from Task have a similar restriction though.

If you mark a type as sendable, you're responsible for implementing a mechanism to ensure mutual exclusion for that type, making sure any variables that aren't value types can't be accessed or modified by multiple threads at the same time.

This is true, but can be very misleading. For example, marking a type with a global actor also makes it Sendable, which is something a lot of people don't realize when first starting with concurrency.

There's no way to use async/await without adjusting the function signature to make it async, which means you’d also have to handle the same issue in the calling function.

Yes true.

The closer a Task is to the UI, the more non-value types you have to mark as Sendable, and the more mechanisms you need to implement.

This, I do not agree with at all.

I think you should attempt to move the initial async context (the Task) as close to the "conceptual" beginning of the work as possible.

Further, I think that, in general, attempting to make reference types Sendable is a mistake unless they are already thread-safe internally. What you want is to not need the types to be Sendable.

The only other alternative is to skip async/await and revert to using closures, possibly with queues to manage thread changes and prevent overwhelming the UI thread.

I think this is getting to the core of the problem. There's nothing going on here that could possibly hang up the main thread. You only need to be concerned with work that is both long-running AND synchronous. The only operation I see that could be long-running is marvelCharacterServiceInput.getCharacter, and that is already async.

Normally, I'd tell you to mark this type @MainActor. But, that could be difficult because I do not see the clients of this API and how they interact. But, so far I see no evidence that you need more concurrency. I think you may need less.

2 Likes

Thank you so much for your reply.

It was very helpful.

1 Like