[Accepted] SE-0392: Custom Actor Executors

Hi everyone. The language workgroup has decided to accept SE-0392: Custom Actor Executors after the second round of review, which ran from April 7...17, 2023. During the second review:

  • Participants raised questions about how this feature's potential role in inhibiting actor reentrancy, for code where allowing reentrance is not desired. The language workgroup agrees with the proposal authors that, while that is an important future direction, it is not this proposal's problem to solve.
  • There was some discussion about the naming of the preconditionIsolated, assertIsolated, and assumeIsolated APIs. Although precondition is not a verb, the language workgroup believes the naming association with precondition() and assert() is important to underline that they have analogous behavior in whether they check or not in debug or release builds. assumeIsolated doesn't have as clear of a precedent, but there wasn't a clearly better alternative, and the proposed name seems acceptable as is.
  • There was some concern about UnownedJobs unsafety, and questions as to whether its variation of runSynchronously and other APIs should have unsafe in the name to make the potential for undefined behavior more apparent. Although UnownedJob unfortunately doesn't have Unsafe in the type name (and it can't be added because of existing ABI constraints), the language workgroup believes the type itself should be seen as inherently unsafe, and like other fundamentally unsafe types such as Unmanaged, it doesn't need to have that unsafety reiterated on every method name. Also, although SE-0392 doesn't formally deprecate UnownedJob yet, the ExecutorJob type will be more prominent, which is safe and uses noncopyability to enforce safety, and developers will be encouraged to use it over UnownedJob as much as possible. When the expressivity of noncopyable types reaches the point that most uses of UnownedJob can be avoided, then we would like to see it formally deprecated.

Thank you to everyone who participated in both rounds of review!

30 Likes

It is still a bit unclear to me if I can build a LIFO structure using a custom executor? A simplified example would help.

actor Logger {
func log(message: String) {
}
}

How can I make sure that of logger is used everywhere that it's messages are logged ordered, meaning the last one in is the last one out?

This proposal is not enough to ensure strong order.

Suspension points still may interleave.

You’d need “non-reentrant actors” to achieve this. This was alluded to in the first ever actors proposal but no implementation progress was made towards them yet.

Workarounds include building your own queue using an async stream… it’s not great. We could at the very least publish a pattern for it, but it’s a topic that should be revisited properly sometime in the future.

3 Likes

Thank you very much for the quick response. I did the workaround in the past and they where confusing and error prone for me. So I now unfortunately abuse @MainActor because especially on linux I noticed inconsistent behaviour when writing to files in an async context, meaning sometimes the files where empty.

This happened in our pipeline that runs on linux but on our dev machines that are macos it never happened. So falling back to @MainActor to solve this issue is dirty. But if like you suggest a best practices could get published somewhere, maybe swift blog, that would be helpful thanks.

The files topic I suspect might be about flush behavior differences between the platforms… but that’d be a different topic rather than this thread. If you have some reproducible sample that’d help investigate.

I do not have reproducible code as it happens in our pipeline 1/3 times. But that is a very elaborate swift code generation library that for my client. But the code I use is very simple and based on GitHub - doozMen/Files: A nicer way to handle files & folders in Swift.

I have asked around about the files issue on the forum Task safe way to write a file asynchronously - #4 by Karl

Basically my code is

import ArgumentParser // https://github.com/apple/swift-argument-parser
import Files // https://github.com/doozMen/Files
struct Write: AsyncParsableCommand {
  func run() async throws {
    let files = try ["foo.swift", "bar.swift"]
      .map { try Folder.current.createFileIfNeeded(withName: $0) }
    
    try await withThrowingTaskGroup(of: Void.self) { group in
      for file in files {
        group.addTask {
          try await Task {
            try file.write("// some data")
          }.value
        }
      }
      try await group.waitForAll()
    }
  }
}

Basically if you use version of Files 4.2.1 that does not use @MainActor for the file write you will get 1 out of 3 times on Ubuntu 22.04 arm64 one of the files that is empty. If you use the version on main from Files it is slower but I never have it.

Oh that would go a long way I guess :heart: