Queues & locks versus Swift concurrency

I'm working on updating my code to resolve all the concurrency warnings so it's ready for Swift 6. But it's complicated.

Some of the issues have been easily solved by adding @MainActor or Sendable to various things. That's been nice.

But some things are more involved. Here's an overview:

It's a Git GUI app (check it out here if you're curious), so I have an object that manages all the repository operations. A repository object has a dispatch queue for long-running operations like git blame or loading the commit history, and things are protected by locks where needed. Thread Sanitizer is happy.

But Swift's strict concurrency checking is not happy. I've been trying to find information or examples on migrating such a setup, but most articles seem to be on the order of "slap @MainActor on this, turn that into an actor, and viola!" but this isn't so simple.

One requirement is that the user must be able to browse the repo's commits and files (ie, perform various read-only operations) while other stuff may be happening in the background (network operations, reacting to file system changes, or calculating the commit graph). So therefore simply turning that object into an actor, and therefore serializing all operations, is not what I want to do. Plus I've got data models built on top of the repo that get tossed between main and background tasks but I'm not sure I can make them Sendable. I could just spread @unchecked Sendable and nonisolated(unsafe) all over but I want to be careful about this.

Are there examples out there of more complex concurrency migrations? Perhaps of starting with a dispatch queue and mutex based solution, and learning how to make Swift concurrency happy?

Is it possible to enumerate the kind of issues (compiler warnings or errors) that you have when interfacing with your repository manager?

When you enumerate those issues, it will help to distinguish the manager from the code that uses it, even if both belong to the same app. Think about the manager as a library embedded in your app. Are the concurrency problems created by the manager api (i.e. they are difficult to use in a way that satisfies the compiler)? Or are the concurrency problems created by the manager api users (i.e. the manager api is sound, but they are misused)?

(Code and errors examples of what’s complicating exactly would help to make suggestions more specific).

For long running background tasks you better to offload Swift’s queues pool by using actor with custom executor, as a general suggestion.

BTW, you also have mixed dispatch queue and locks here, that from (just this description) looks like some of this things are redundant? Or — which is assumption below — is the sign that your object manages several distinct things at once.

Couldn’t be the case that this object overloaded with tasks? It seems from your description it has to do so many things.

Swift Concurrency might enforce different design, in particular with more clearer isolation boundaries. So it might be the case you actually would need to rethink your components. Probably, by separating this repository object into a few actors that responsible for various things.

If your data models ensure internal thread safety (e.g. via locks), then unchecked is the way to go. Otherwise, you need to rethink this models as well by either protecting with locks or re-modeling.

Thanks for your thoughts. I guess the answer is that, in trying to satisfy Thread Sanitizer, I've been a bit to haphazard in throwing locks here and there, and now Swift is forcing me to think things through at a higher level. Which is, in the end, a good thing :slight_smile:

Thread Sanitizer helps make sure my code is thread safe as it currently stands. Swift concurrency, on the other hand, helps make my code resilient against future changes.