[Pitch] @Concurrent macro

This is the updated Pitch [Pitch] @Concurrent macro - #16 by EngOmarElsayed

Macro SPM: [Pitch] @Concurrent macro - #21 by EngOmarElsayed

Hey :waving_hand: everyone,

I started to adopt strict concurrency in my apps and at work. One of the most used annotations for me is @concurrent because all the apps I work on have some kind of heavy processing that should be executed on the background executor, something like network calls for example.

For example:

final class RemoteDataSource {
  @concurrent func fetch() async {}
}

I want all the functions in the RemoteDataSource to have the @concurrent to make sure all of them will be executed on the background executor, but there is no way to tell the compiler that all the functions in RemoteDataSource should have @concurrent.

I have to add it manually for every function I create which opens the door for errors and mistakes because I can easily forget it.

So I created a @Concurrent macro that does that for me and I wanted to propose it to be added to the Swift language. The @Concurrent macro can be added to extensions, classes, structs, enums and actors (still thinking about if it should be added to actors but I think it doesn't make sense, I want to see what you think), what it does is simple, it adds @concurrent to all the functions inside that declaration for example:

@Concurrent
final class RemoteDataSource {
// @concurrent this will be added to function by the macro
  func fetch() async {}
}

struct FetchDataUseCase {
//....
}

@Concurrent
extension FetchDataUseCase {
  // macro will add @concurrent to the functions in this extension only
  func fetch() async {}
}

This is a simple syntax sugar to add to the language but could make a difference and make things easier for developers.
What do you think? Especially about the Actor part. Excited to write the proposal for this one :sweat_smile:

1 Like

Do you have any additional motivating examples where such a macro might prove useful? I'm struggling to think of one myself, as rarely (possibly never) have I encountered a type which requires each and every one of its methods to run on the global concurrent executor.

Also: such an API would warrant introducing some sort of opt-out @NonConcurrent sibling macro, and at that point I think the language would be better off with just reaching for the existing @concurrent attribute when necessary.

Edit: It just dawned on me that the language, more or less, already provides similar functionality via custom global actors. There's nothing holding one back from doing something like:

@YourGlobalActor
struct FetchDataUseCase {
//....
}
1 Like

A global actor is not quite equivalent. All of those actor functions would be serialized to the global actor, but an @concurent function is not serialized to anything.

But aside from that, I agree that I think it would be useful to better understand why such a facility would be useful. The motivating example, network requests, are (I have to imagine) asynchronous. I think only synchronous work could really benefit here. I could see synchronously preparing a network request and/or processing the result maybe?

1 Like

A global actor is not quite equivalent. All of those actor functions would be serialized to the global actor, but an @concurent function is not serialized to anything.

Yup, agreed. I should have further emphasized the "more or less, similar" part of my post. :slightly_smiling_face:

But aside from that, I agree that I think it would be useful to better understand why such a facility would be useful.

Indeed. The only examples that come to mind (types which hold database handles, or performing excessive I/O) already fit neatly into the actor paradigm.

2 Likes

From what I understand (please correct me if I am wrong) in swift 6, if you have something like this:

struct FetchDataUseCase {
  func fetch() async {}
}

This makes FetchDataUseCase.fetch nonisolated which means that calling FetchDataUseCase.fetch will be isolated on the callers actor so the way to prevent that and to grante it will be excuted on background excutor is only by adding @concurrent anotation to method, this because there is a lot of cases where you don’t want to have method like FetchDataUseCase.fetch to be on MainActor by mistake and also you don’t want to use actors because actors will make it's calling serialized.

With that in mind, there is a lot of cases where I want to have this grante for all the methods in a certain object or extension like objects that have network or bussiness logic or pre/post-Data processing.

This is where the @Concurrent comes in, it makes it easier to have this grante for all the methods in a certain object or extension without doing it manually.

struct ProcessDataUseCase {
   func processThis() async {
       // Some logic don’t need background grante …
       await fetch()
       await fetch2()
       await proceess()
    }
}

@Concurrent
extension FetchDataUseCase {
  func fetch() async {}
  func fetch2() async {}
  func proceess() async {}
}

// Before 

extension FetchDataUseCase {
  @concurrent func fetch() async {}
  @concurrent func fetch2() async {}
  @concurrent func proceess() async {}
}

There is no need for @NonConcurrent sibling macro because if you want only certain methods to be @concurrent create an extension of that object and add @Concurrent to it. If I understand you correctly there is no need at all for @NonConcurrent if the @Concurrent macro is created.

Excatly, objects that are responsible for processing results of network request could benefit from that too or even reading from a certain data source.

Yes, the data source it self will be an actor to protect the data integrity but there is a lot of cases where you create a layer on top of it to process the data you read to display it on UI, such processing can be heavy if it was on the MainActor by mistake so adding @concurrent will grante for us that such a processing is always on the background.

And this is where @Concurrent macro comes in, it is just a syntax sugar for this:

extension FetchDataUseCase {
  @concurrent func fetch() async {}
  @concurrent func fetch2() async {}
  @concurrent func proceess() async {}
}

Networking is, in general, an asynchronous operation. So it would not typically benefit from @concurrent at all. Neither would any internally-invoked isolated function calls. However, the encoding and decoding of the values transferred might be both expensive and synchronous, so they could potentially benefit.

I just want to make sure that it is clear in your mind that serializing to a single actor (MainActor or otherwise) only matters for synchronous work. That means @concurrent is generally only useful for managing synchronous work. This is why I'm interested in more details about the motivation.

3 Likes

In your examples I only see the proceess procedure as a potential candidate for marking as @concurrent. All the various fetches which you demonstrate may simply remain async, and just call into a subsystem with a single entry-point which is itself either marked as @concurrent or perhaps bound to a background thread in an alternative fashion.

I don’t want to derail your pitch and will let others chime in, but I still don’t see a strong argument for introducing this.

You are totally right, I was considering network as a usecase in the motivation because most of the time I always do some kind of data processing before network so I was mistaken to consider it as network call it self, I will re-write the pitch because releasing this made me understand why there is confusion and at the same time now I understand more why it will be useful a lot but I need to articalte it in the right way using the right terms.

Thanks so much Matt.

2 Likes

You are right, the examples I used aren’t strong enough and at the same time not quite right, so let me re-write it.

But thanks so much your feedback helped this is the first time for me to do such thing :sweat_smile:

3 Likes

Should I create a new thread with the rewritten version or should I add it here ?

I think continuing with this thread makes sense for the discussion

1 Like

I think that if I had a need to run a large number of asynchronous functions off the calling context's actor, disabling (or not enabling) the NonisolatedNonsendingByDefault feature flag would be my preferred choice.

Perhaps an alternative approach would be to offer options such as "concurrent" and "dynamic" that can be easily configured when creating a package or project.

I think this would be a good idea in general: when creating a new Xcode project, there could be options to set the default isolation (@MainActor or nil) and to specify where non-isolated asynchronous functions run by default. Similarly, for a Swift package, each target could have preconfigured Swift settings in the Package.swift file that can be easily modified at any point.

What you are suggesting here is making NonisolatedNonsendingByDefault another language dialect, which would result in four possible combinations along with default isolation.

I'm willing to discuss pretty much anything. But the motivation would have to be extremely, overwhelming compelling. So far, I don't think we've see that.

Could you elaborate? Isn’t this already the reality?

Just looking at this piece of code, we can’t be sure whether f is @MainActor, @concurrent, or nonisolated(nonsending):

func f() async {}

Yes, you are right that there is a temporary dialect introduced here. But eventually there will be a new language mode, say 7, where NonisolaredNonsendingByDefault will be unconditionally enabled. It is an upcoming feature. This is distinct from default isolation, which is not.

Hey :waving_hand: everyone,

This is an updated version of the pitch with more precise terminology and better examples.

Motivation

Before Swift 6.2, a nonisolated async function always ran on the global concurrent executor:

struct ImageCompressor {
  func compress(_ image: Data) async -> Data {
    // Before SE-0461: always runs on the global concurrent executor
  }
}

With SE-0461 (enabled via the NonisolatedNonsendingByDefault upcoming feature, part of Xcode's "Approachable Concurrency" setting), nonisolated async functions now run on the caller's actor by default. If compress(_:) is called from the main actor, all of its synchronous work now happens on the main actor.

To get the old behavior back, we annotate the function with @concurrent:

struct ImageCompressor {
  @concurrent func compress(_ image: Data) async -> Data { … }
}

This is the right default for most code. But some types exist specifically to do heavy CPU-bound work — image compression, decoding large JSON payloads, hashing files, data processing in use cases after fetching from a backend. For these types, every async method needs @concurrent:

struct ImageCompressor {
  @concurrent func compress(_ image: Data) async -> Data { … }
  @concurrent func resize(_ image: Data, to size: CGSize) async -> Data { … }
  @concurrent private func encode(_ image: CGImage) async -> Data { … }
}

This is repetitive and easy to get wrong. Forgetting a single @concurrent doesn't produce a compiler error — it silently runs heavy work on the caller's actor (often the main actor), which is exactly the kind of performance bug the annotation exists to prevent.

Proposed solution

I propose a @Concurrent attached macro that can be applied to struct, class, and enum declarations, as well as extensions. It adds the @concurrent attribute to every nonisolated async method in that declaration:

@Concurrent
struct ImageCompressor {
  func compress(_ image: Data) async -> Data { ... }   // macro adds @concurrent
  private func encode(_ image: CGImage) async -> Data { ... } // macro adds @concurrent
}

It can also be scoped to a single extension:

struct FetchDataUseCase { … }

@Concurrent
extension FetchDataUseCase {
  // macro adds @concurrent only to async methods in this extension
  func process(_ response: Response) async -> Model { … }
}

Detailed behavior

  • Only async methods are affected. Synchronous methods are untouched — they run wherever they're called, exactly as today. @concurrent only changes where nonisolated async functions execute, so the macro skips synchronous methods entirely.
  • Per-method opt-out wins. A method explicitly marked nonisolated(nonsending), @MainActor, or with any other explicit isolation would be skipped by the macro.
  • Not allowed on actors. Applying it to an actor contradicts the whole point of actor isolation, so the macro would emit an error there.
  • Naming. I'm aware @Concurrent differs from @concurrent only by capitalization, and I'm open to bikeshedding the name (e.g. @ConcurrentMembers) if that's a concern.

Alternatives considered

  • Status quo: annotate every method manually. Works, but it's boilerplate with a silent failure mode.
  • Extending the @concurrent attribute itself to types and extensions: arguably the most consistent design, but it requires compiler changes, while a macro can be implemented and shipped without touching type checking. If the community prefers the attribute route, I'd be happy to pursue that direction instead.

I keep running into this pattern at work — use cases doing data processing after backend calls, image compression pipelines, and similar CPU-bound helpers — so I think this would benefit a lot of codebases adopting Swift 6.2's new defaults.

What do you think?

I would propose you kick this off as a package, and if we see a lot of adoption of it let's consider for stdlib. The thing with stdlib is that it's really hard to change anything about it, so such things absolutely can and should just live as packages.

I understand it's cool to push features into stdlib, but it's equally cool to just have a very cool and popular helpful macro package of your own -- it's even better for the ecosystem to be honest, encouraging exploration and experimentation with small packages is really beneficial to everyone -- you can evolve it at will, get your name out there, and people can "just fix it" immediately if there's some issue, rather than wait multiple months.

So I'd nudge this one towards just a package, I'm sure folks will enjoy and recommend it :slight_smile:

5 Likes

Do you have anything in mind, I should consider or tips ?

Not sure what you mean? Just make a swift package as a github repo, let people use it -> win/win situation :slight_smile:

I mean things I should consider while implemnting SPM in the macro behavior I didn’t mention in the Detailed behavior