[Concurrency][Pitch] Task Local Values

Thanks a lot for the review, Chris - great to have some in-depth discussion on this :slight_smile:

It's just on time, as this was first posted to highlight missing things in the Task proposal (which you also notice in this review :slight_smile:) and since this was posted the missing things we ironed out in Task APIs and the upcoming Structured Concurrency Pitch #3 will have all APIs which enable this proposal to be implemented properly (i.e. also working from within sync functions).

At the time of writing this in December it wasn't clear what will happen with the Task / unsafeCurrent APIs if they'll happen or not, so the linked complete implementation and pitch were not able to implement the full potential of this pitch yet -- thankfully now soon we'll be able to as the calling convention and ABI of tasks/executors is changing to manage a thread local for the Task so it can be used for Task.unsafeCurrent which enables great updates to these APIs).

So timing is good, thanks for chiming in, let's dive in...


Usage in synchronous functions (coming very soon)

Yeah, I absolutely agree!

Back in December when this was implemented and pitched I had my hands tied somewhat by being unable to implement this given the restrictive state of the Task APIs. Access to tasks is currently implemented as "magical extra parameter to async functions," making it impossible to implement accessing a task from a sync function. It is indeed a huge, feature-breaking even, limitation...

Thankfully, during Pitch #2 of Structure Concurrency in the last weeks (this design) we were able to hash out the missing and misaligned APIs which resulted in: Task as struct, static non-async funcs, and UnsafeCurrentTask by ktoso ยท Pull Request #48 ยท DougGregor/swift-evolution ยท GitHub which will be Pitch #3 of those APIs.

This implies changes in ABI/calling-convention how a Task is passed around, so it's still not implementable until that is changed, but at least the team seems to be agreeing on adding Task.unsafeCurrent so finally we can express task locals within synchronous functions and this proposal becomes wholly useful then :partying_face:

The writeup above I hope answers this: this was the only way to implement this. It sucks, and it will be fixed in Pitch #3 with introduction of Task.unsafeCurrent :+1:

Indeed, Task.local(\.x) becomes non-async then :+1:


withLocal { ... } spelling

Reads are trivial and solved, as you say.

It is the "binds" that must be structured. The reason it can't be "just a setter" you kind of said yourself -- it would not be structured.

As usual... with enough compiler magic, everything is doable, and we could implement it as:

func foo() {
  // yeah we could even do Task.local.foo = 222, 
  // I did exactly such API for swift-distributed-tracing's attributes;
  Task.local(\.foo) = 222 // "pushes" a binding to task-local bindings stack

  // every binding magically generates:
  // @synthesized defer { Task._popTaskLocalValue() }
  // which must "pop" the binding off the task-local bindings as we return

  assert(Task.local == 222)
}

Task.local(\.foo) = 111
foo() // NOT a child task; foo is 222 inside there.
assert(Task.local(\.foo) == 111) // this assertion MUST hold

So while this may be "less noise" it is definitely a lot more magic and surprising: we're writing to something that looks static, but it'll automagically isn't and unbinds at the end of the scope...

Whereas the with style has lots of prior art and feels like pretty normal day-to-day Swift. There is no need to explain any magical semantics when presenting the following code:

func foo() {
  let before = Task.local(\.foo)

  Task.with(\.foo, boundTo: 222) {
    assert(Task.local(\.foo) == 222)
  }

  assert(Task.local(\.foo) == before)
}

This both enforces the right semantics, and informs the programmer with the shape of the API what's going to happen here. Is pretty much "normal" swift code and no-one has to jump through learning the specialized magical semantics of scoped task local values to understand this. Therefore, I think, the body/scope of withLocal(...) {...} is not noise, but helpful information.

So I really believe the less magic version is much preferable here, because it is more true to what is actually happening.

"currentActor"

While actors are out of scope for this proposal, it should also be possible to get a pointer to the current actor (or nil if not on one) from sync code as well for the same reason.

Yeah that's absolutely useful and probably one of the main things we'd spend time on when instrumenting Akka :wink: It helps a lot in debugging/logging and just understanding what's going on in the system.

+1 overall, but let's keep this separate from this proposal. I do suspect though we'll end up having such thing. Probably through the Task.current APIs :thinking:


That's a specific request from @John_McCall -- this isn't really about the task local values but about the general existence of an "give me a handle with full access to everything, even APIs which should never be called from a different task". We can get into the details of that Unsafe... name and type in the Pitch #3 of Structured Concurrency which should be posted this week I hope.

The actual unsafety of it comes not really from the Task.unsafeCurrent but the fact someone could escape/store this value and then use it from a different task, that's the unsafeness we're concerned about. John wanted to eventually make a type that's "un-movable" which then could be safe, since we'd prevent people from doing this. ConcurrentValue things don't really solve this -- people could store it in some property and then touch the same property, get that UnsafeCurrentTask from a different task and poke at it unsafely.

Will do, by now the ConcurrentValue is fleshed out enough and looking good that this seems like a good time to refer to it. I agree that restricting values stored in task locals to ConcurrentValue is a good idea :+1:


Thanks for the catch here; my self-deprecating humor backfired here, will reword this -- thanks for pointing it out :+1:

Please let me know if this makes sense -- in the meantime I think it's time to prepare Pitch #2 here and base it on the existence of the upcoming Task.unsafeCurrent :slight_smile:

1 Like