Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there something equivalent to thread-local storage, but for coroutines? #597

Open
georgikoyrushki95 opened this issue Jan 19, 2024 · 1 comment

Comments

@georgikoyrushki95
Copy link
Contributor

Let's call it something like LocalContext for now. And let's make the simplification we're dealing with a unifex::task.

The basic requirements I am looking for are:

  1. When 2 unrelated tasks interleave on the same executor, their LocalContexts are isolated from one another.
  2. When there's a parent-child relationship between 2 tasks (e.g. taskA co_awaits on taskB), taskA's context is propagated onto taskB's contexts.
  3. LocalContexts resources are managed by the containing tasks (i.e. they get destroyed when the underlying coroutine gets destroyed).

Python has ContextVars that is more or less what I describe above. It has the added advantage that it works also in cases where you need plain thread local storage.

I did a little bit of search some time ago, but didn't see anything in the works or being proposed.

@ccotter
Copy link
Contributor

ccotter commented Jan 19, 2024

I have something cooked up that uses task's scheduler affinity to save/restore a context on reschedule. A sketch of the API looks like

template <class T> struct ContextVar {
    T& get(); // Return the value assigned to this variable within the current context.
              // Raise when no value is assigned to the currrent context
    
    void set(T); // Assign the value to the current context.
};

static ContextVar<int> s_data;

task<int> inner() {
    co_await some_io_operation();
    co_return s_data.get();
}

task<void> outer(int i) {
    s_data.set(i+1);
    
    int value = co_await inner();
    assert(value == i+1);
}

task<void> main_event_loop() {
    async_scope scope;
    while (optional<int> data = co_await read_data()) {
        scope.spawn_on(ContextVarsScheduler{Context::copy()}, outer(*data));
    }
    co_await scope.complete();
}

If there's interest, I can share the entire implementation. The quirks/downsides to this approach are

  1. If a child task re-schedules somewhere in the middle of a call chain, that coroutine and subsequently spawned grandchild tasks no longer participates in the ContextVars re-schedule.
  2. Doesn't work with non-scheduler affine coroutines
  3. Spawning with other types like async_scope require explicit code to propagate the ContextVars scheduler. This happens magically for free with task's scheduler affinity, but does not come for free with async_scope etc.
  4. Requires composing the ContextVarsScheduler with the "real" scheduler (e.g., specific thread pool, manual_event_loop, or other). I have a ComposedScheduler(Schedulers...) scheduler that composes 2 or more schedulers into one.

Another thought would be to integrate this directly into the loops (e.g. manual_event_loop) themselves (as Python does), which avoids the downsides above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants