Ambiguity in the huge number of concurrency attributes

All what is written below is probably just my misunderstanding, but I will try to explain my experience and I really want to clarify a lot of things for myself.


The process of Incremental Migration to Concurrency Checking is sometimes very difficult because of the huge number of added attributes and many related problems:

  • some attributes are introduced, but afterwards replaced by others. This is not a problem when you can get the available information about it
  • Sometimes you have to search for information by bits and pieces, looking at posts on this forum, pr`s on github, but there is no single page where you can easily find information
  • lack of good examples
  • there is no single page with the most known bugs. There aren't many of them, but if you know about them in advance, you don't have to sit for hours and think that you've got something wrong. Radar and issues on github have a lot of them, but it's usually pretty hard to isolate anything from there

Questions to which I cannot find an exhaustive answer:

  1. @MainActor(unsafe) it's the same as @preconcurrency @MainActor ?

  2. How does @MainActor(unsafe) it really work and is its behavior correct in this example:

// -strict-concurrency=targeted or minimal
// -enable-actor-data-race-checks

@MainActor(unsafe)
final class SomeViewModel {
  let value1 = 0
  var value2 = 0
  func getValue2() -> Int { value2 }
  func setValue2(_ newValue: Int) { value2 = newValue }
}

func doSomething(with viewModel: SomeViewModel) {
  _ = viewModel.value1
  _ = viewModel.value2
  _ = viewModel.getValue2()
  viewModel.setValue2(3)
}
// -strict-concurrency=complete
// -enable-actor-data-race-checks

@MainActor(unsafe)
final class SomeViewModel {
  let value1 = 0
  var value2 = 0
  func getValue2() -> Int { value2 }
  func setValue2(_ newValue: Int) { value2 = newValue }
}

func doSomething2(with viewModel: SomeViewModel) {
  _ = viewModel.value1
  _ = viewModel.value2 // ERROR: Property 'value2' isolated to global actor 'MainActor' can not be referenced from this synchronous context
  _ = viewModel.getValue2() // ERROR: Call to main actor-isolated instance method 'getValue2()' in a synchronous nonisolated context
  viewModel.setValue2(3) // ERROR: Call to main actor-isolated instance method 'setValue2' in a synchronous nonisolated context
}
  1. Is the @asyncHandler attribute valid?

  2. What the @_unsafeMainActor, @_unsafeSendable attributes really do?

  3. Is @_unsafeMainActor an equivalent attribute for a function as opposed to @MainActor(unsafe)?

  4. Does the -warn-concurrency flag still apply, or does the new flag -strict-concurrency=minimal/targeted/complete completely replace it?

  5. Is it true that at this point there is no way to do it without warning and no extra hop with Task:

@MainActor(unsafe) 
func foo() {}

RunLoop.main.perform {
  foo() // Call to main actor-isolated global function 'foo()' in a synchronous nonisolated context; this is an error in Swift 6
}

Unless you use assumeOnMainActorExecutor, but this is only available in swift 5.9 for now.

1 Like

This is an unfortunate byproduct of there being no single document that describes Swift's concurrency feature and is kept up to date. Everything you're saying is true. In short:

  • You should pretty much never need to use @MainActor(unsafe). Just use @MainActor.
  • @asyncHandler is a very old attribute that was never adopted.
  • Generally stay away from the @_unsafe attributes. They disable actual compiler checking and will result in runtime crashes if your assumption is incorrect.
  • Use -strict-concurrency. It also maps to Xcode's setting.
  • Yes, many of Apple's APIs haven't been updated to work well with Swift concurrency. Many of their types aren't properly Sendable and there are various APIs, like RunLoop, which don't integrate well.
5 Likes