Swift Concurrency: Feedback Wanted!

Today Apple released the first beta of Xcode 13. This includes Swift 5.5, with support for language-native concurrency. Most of these features have already been accepted as evolution proposals, as well as a few that are still in review.

Swift Concurrency is a huge feature, by far the biggest change to the language that has been through the evolution process. To keep things manageable, the feature has been broken up into individual proposals for async/await, actors, Objective-C interoperability etc. But it’s also important to consider how all the features work together. Even where proposals have been accepted, further feedback about how they interact in practice is still welcome.

Evaluating the impact of the feature on real world applications is also key. Xcode 13 includes an annotated SDK that expands the number of APIs that will work natively with Swift Concurrency.

We’d love people to post their experiences of trying out the feature – what went well and what was more challenging – here in this thread once you have a chance to try them out.

For clarification on how to use the new concurrency features, or talk about common patterns you’ve found useful, the Using Swift section of the forums are also a great place to share ideas and ask questions.

For reference, here is the full list of concurrency-related proposals included in the first beta. Note, in some cases there can be lag between a proposal being updated in open source and it making it into the build, so some proposal amendments are not reflected in the first beta. For example, beta 1 still requires async { } to launch an unstructured async task rather than the now-proposed Task { }. The task-local values proposal also has amendments that will appear in a later beta.

67 Likes

15 posts were split to a new topic: Will Swift Concurrency deploy back to older OSs?

Thank you everyone for your work on the concurrency features! This is a massive amount of work and it's great to see the first part of it start to land.

Considering that UIViewController is now annotated with @MainActor, and that global actor annotations propagate down to subclasses (implying that all view controllers are now implicitly isolated to the global MainActor), what's the story for interacting with view controllers when targeting <iOS 15? Wouldn't this imply that we'd need to wrap calls from outside an @MainActor isolated context in async { } / Task { } (APIs which won't be available when deploying back to <iOS 15 as far as I can tell)?

1 Like

what's the story for interacting with view controllers when targeting <iOS 15?

@MainActor and other concurrency annotations are probably a no-op for targets < iOS 15

2 Likes

AFAICT this isn't the case:

@MainActor
class ViewController: UIViewController {
  func mainActorIsolated() {}
  func doSomething() {
    DispatchQueue.global().async {
      self.mainActorIsolated()
    }
  }
}

The above fails to compile when targeting <15, (Call to main actor-isolated instance method 'mainActorIsolated()' in a synchronous nonisolated context), as you'd hope. Using async { rather than DispatchQueue.global().async { fails to build since the API is not available.

The following builds fine:

@MainActor
class ViewController: UIViewController {
  func mainActorIsolated() {}
  func doSomething() {
    DispatchQueue.main.async {
      self.mainActorIsolated()
    }
  }
}

I'm wondering if there's something going on to inform the compiler that DispatchQueue.main.async { executions are isolated to the global MainActor? (Unsure how this would be done since DispatchQueue.main is still just returning a DispatchQueue).

So far it looks like only on new Apple platforms.

eg:

detach { await someAsyncCode() }

Throws a compile time macOS12.0/iOS15.0 needed

same with

await withUnsafeContinuation { c in
        oldCallbackBasedMethod { result in
            c.resume(returning: result)
        }
    }

which makes the whole thing a bit useless for anyone supporting 'older' OS versions (you know, like those released in 2020...) - another feature to wait a year or three before it can be used with confidence in shipping code rather than local tinkering/hobbies.

More fun - trying to find out if it was possible to do anything with await/async without hitting a 'needs new version' error this will compile with a target of macOS 11.3 and then crash at run time:

import Foundation

@main
struct S
{
	static func main() async throws
	{
		let result = await doSomething()
		print("result: \(result)")
	}
	
	// Not actually async - but declares itself async to see what happens.
	static func doSomething() async -> Int
	{
		var i = 0
		for j in 0..<1_000_000
		{
			i += j&17
		}
		
		return i
	}
}
1 Like

What I meant was that U thought the @MainActor annotation for UIViewController would not exist if you target < iOS 15. But if your code compiles, then it seems that it does.

This was certainly confusing to me watching the WWDC sessions. I found myself asking what this is, since it looks like it’s an init with a trailing closure, but it is lowercase, so it’s unlike any Swift constructor I’ve ever seen.

It's not that uncommon when you think about it... zip and lazy.map are functions that also "construct" types (a zip sequence and lazy map sequence type respectively).

Nonetheless, the newer proposed Task { } explicitly constructs a task with its initializer which is generally more conventional.

10 Likes

This is the beginning of a new voyage.
Congratulations and thanks to all the contributors.

1 Like

The following code doesn't work.

struct Pair <A, B> { var a: A, b: B }
@available(macOS 12.0, *)
func asyncTests () async {
   var t = Pair(a: 0, b: "")
   await withTaskGroup(of: Void.self, body: { group in
      group.async {
         t.a += 1
      }
      group.async {
         t.b.append("!")
      }
   })
}

The compiler says 'Mutation of captured var 't' in concurrently-executing code'. I can clearly see that no race condition will ever occur in this code, apparently, compiler can't. How to approach this one?
If such simple task cannot be handled properly, what that 'easy to use' noise talking was about?

Currently, you can only use capture-by-value for the these @Sendable closures (SE-0302).

Experimenting more with @MainActor and view controllers, the following compiles and prints main thread: false.

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    DispatchQueue.global().async { self.mainActorIsolated() }
  }

  @MainActor
  func mainActorIsolated() { print("main thread: \(Thread.isMainThread)") }
}

It does not compile for a similar class that does not inherit from UIViewController (Call to main actor-isolated instance method 'mainActorIsolated()' in a synchronous nonisolated context). Is this a bug?

It's possible that, in the future, Swift might gain the ability to allow captures of vars in @Sendable functions as long as the var is immutable before the function is ever possibly called. That would be a safe extension to the language.

Modifying a capture in a @Sendable function is not allowed because the function may be run concurrently with itself. async happens to not do this with its argument, and it's possible that in the future Swift will gain the ability to statically restrict a function so that the compiler can know that's true. That would probably require Swift to support affine ("move-only") types, like Rust, so that it can prove that all calls to the function are exclusive with each other. This would allow mutation of variables that are not captured by other functions from the original scope.

That wouldn't be enough to allow your example, however. As a general rule, in Swift, an access to a part of a value type is considered to be an access to the whole value. That means that your two closures are considered to be concurrently modifying t, and this is indeed the rule that is enforced by exclusivity today. It is quite unlikely that Swift would weaken this rule to allow what you've written. That extension would have to be extremely narrow, and it would tend to lead programmers into design corners that they cannot escape. Programmers will likely always have to break this value apart.

Stepping back, the standard for "easy to use" is not and could never be "it is impossible to come up with an example that a programmer can prove is valid but which the compiler rejects". The standard is whether it is easy to solve your problems while working within the inevitable constraints of the system. Swift's concurrency may or may not meet that standard, but toy examples aren't very helpful for deciding that. It would be better to understand what code you'd like to write that does actually look like this.

18 Likes

That example is basic indeed, though it's already enough to pin the problem – mutating aggregates is going to be troublesome. I just tried to come up with a substitute to concurrentPerform and didn't manage, perhaps you could enlighten me.

I see that these constraints is the thing that presses me to come with workarounds to things that in other settings are easier to accomplish: in c + pthreads it's doable, in swift I don't even see model yet. Why I cannot grow an array from multiple tasks, for example? It was hacked to be thread-safe, no?

This addition ain't gonna fix problems, John. What would is something like 'borrowed until awaited'

Array has value semantic. It is safe to pass it by value to another thread because it will be copied and each thread will have its own copy. The COW optimisation that is performed under the hood is indeed thread safe.

But passing an array by ref to multiple threads and accessing it from multiple threads is not safe.

C + pthreads can do a lot, but it's also a lot harder to use than the Swift concurrency model.

FWIW, the information required for that particular code to be obviously safe (no overlapping access in particular) are still missing:

  • That group.async.operation does not outlive group lifetime,
  • That group does not/can not escape withTaskGroup.body,

also as John said, that

  • t.a access and t.b can have overlap accessed, and
  • Each group.async.operation is not run in concurrent with itself.

These would be visible to human readers (with difficulty or otherwise), but I don't think the compiler is keeping track of any of this.

2 Likes

Growing an array from multiple threads is definitely not thread-safe.

Knowing that a variable is only accessed concurrently in structured subtasks and so can be safely used again after they complete is a different problem, but yes, one that would also be necessary to solve.

In the meantime, it's almost always possible (and often cleaner) to write these things in a more functional style so that the subtasks produce values that are accumulated rather than directly mutating something.

4 Likes

Would it also be possible to drop to Unsafe*Pointer for the mutating pattern above? I vaguely remember that access to Unsafe*Pointer is simply unchecked, though I can't find the source. (The proposals on concurrency has grown quite numerous, frankly speaking.)