SE-0323: Asynchronous Main Semantics

Hello Swift community,

The review of SE-0323 "`Asynchronous Main Semantics" begins now and runs through September 27, 2021.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

https://github.com/apple/swift-evolution/blob/master/process.md

Thank you,

Doug Gregor
Review Manager

28 Likes

If I'm understanding this correctly, it means that an async main which doesn't await will become exactly equivalent to a regular, synchronous main -- as opposed to the current situation, where there are subtle differences.

If so, that makes complete sense. I think that's what Swift developers would expect. So +1, let's do it.

Having the main function implicitly annotated as executing within the main actor's concurrency domain seems obvious. It seems like an oversight that it isn't already like that.

16 Likes

it means that an async main which doesn't await will become exactly equivalent to a regular, synchronous main

Yep, that was one of the intentions behind this change.

2 Likes

Overall +1
Thank you for your great improvement!

I want to ask about the task execution after the Main.main is completed.

The current implementation on the main branch calls exit(0) after the Main.main is completed.

Think this kind of code creating unstructured tasks in static func main() async.

@main struct Main {
  static func main() async {
    Task {
      try await Task.sleep(nanoseconds: 1000000000)
      print("World")
    }
    print("Hello")
  }
}

If using dispatch_main based executor, the main function should call exit at the end because dispatch_main never returns control.
So this code only print "Hello" and exit with 0 before printing "World". But it's a libdispatch specific limitation, and it's not clear to everyone.

Could you explain the exit process in main and why it's necessary?

The exit is there to ensure that the runloop stops at the end of the main function. Without it, the runloop keeps going and the program never finishes.

Tasks are unstructured, and as such, are fire-and-forget. If you want to ensure that your task runs, you can await it in the main function like so:

@main struct Main {
  static func main() async {
    let t = Task {
      try await Task.sleep(nanoseconds: 1000000000)
      print("World")
    }
  print("Hello")
  await t.value
}

+1 from me - seems like a sensible change.

Would this type of implementation strategy also be applicable for other @MainActor, void-returning methods? I'm thinking of things like UIKit IBAction, etc. I think this was possibly what @asyncHandler was going to do.

Popping over here to share some positive feedback since the thread has been so quiet :wink:


I read the proposal a few times and internalized the rules and thought about if there would be any weird interactions etc.

This looks like a solid improvement and I don't see any issues with it :+1:

I also thought about any future potential weird ideas, like setting the "default pool" during the main execution but before any async work is encountered could also work... So that's all cool. No idea if we'd want this, but it might be a way for limited/single-threaded runtimes to swap out the default executor if they wanted to... Again, no idea if we need this, but it seems we could if we needed to.

3 Likes

+1. I agree with Konrad on all points.

I have mostly been holding off on testing async/await; mostly because I'm waiting for some frameworks to fully adopt it outside of Apple's.

If I were to just start using it and main didn't function as described in this proposal it would first of all surprise me, so generally in favour of making it actually work the way I'd expect it to.

I think introducing a setup() function would simply make it more complicated for anyone starting out. The other considered approach with 2 main functions similarly sounds like complicating things unnecessarily to me.

Generally speaking if I had to change some default behaviour of the async part of the code I wouldn't be very surprised if I had to configure that in the body of main somehow either.

tl;dr +1

This proposal has been accepted, thanks everyone!

Doug

7 Likes