Re: Prototype coroutine support for discussion

14 views
Skip to first unread message

Erik Jensen

unread,
Feb 6, 2025, 8:19:59 PMFeb 6
to Alex Clarke, schedu...@chromium.org, Hans Wennborg, cxx, Peter Kasting, etie...@google.com, ksm...@brave.com
+schedu...@chromium.org again, since it seems it got dropped.

On Thu, Feb 6, 2025 at 1:09 PM Erik Jensen <rkj...@chromium.org> wrote:
(Changing the subject since the thread is no longer about experimenting in //remoting)

On Wed, Feb 5, 2025 at 9:10 AM Alex Clarke <alexc...@google.com> wrote:
Interesting. I'm not sure I count as an expert either, although I did once work on chrome scheduling.  Anyway FWIW I think it should be possible to write something like this:

...
   SomeClass result = AWAIT_PROMISE(MyFunction(arg1, arg2), some_task_runner);
   UseResult(result);
...

Promise<SomeClass> MyFunction(int arg1, int arg2) {
   co_return SomeClass.DoSomething(arg1, arg2);
}

As I mentioned in a C++ side project I've got a JS like promise system and I believe it would be simple to get something like that executing on task runners under the hood.

If I'm understanding correctly, with my prototype that code would look like this:

  SomeClass result = co_await some_task_runner.PostTaskAndAwaitResult(
      base::BindOnce(&MyFunction, arg1, arg2));
  UseResult(result);

Async<SomeClass> MyFunction(int arg1, int arg2) {
  co_return SomeClass::DoSomething(arg1, arg2);
}

In this case, where one just needs to execute a synchronous method on another runner, one could also eliminate MyFunction altogether and just do

  SomeClass result = co_await some_task_runner.PostTaskAndAwaitResult(
      base::BindOnce(&SomeClass::DoSomething, arg1, arg2));
  UseResult(result);

I was originally considering allowing something like this:

  SomeClass result = co_await MyFunction(arg1, arg2).OnRunner(some_task_runner);
  UseResult(result);

However, one could get into sticky situations with pointers:

Async<void> TakesReference(int& value) {
  // Does asynchronous processing that updates value.
}

Async<void> SomeClass::DoTheThing() {
  int local_value = 0;

  // Sound, because local_value lives in the coroutine state. It is guaranteed
  // to outlive MyFunction's execution on other_task_runner_. Since each
  // DoTheThing() invocation gets its own copy, no other code on this sequence
  // will be trying to access it in the interim.
  co_await TakesReference(local_value).OnRunner(other_task_runner_);

  // Sound¹, because MyFunction executes as part of the same asynchronous task
  // on the same runner. If this was bound to a WeakPtr, the task will be
  // canceled if the WeakPtr becomes invalid while suspended within
  // TakesReference().
  co_await TakesReference(member_value_);

  // Dangerous: member_value_ could be simultaneously accessed from this
  // sequence and from other_task_runner_. Further, if this was bound to a
  // WeakPtr, TakesReference() might try to access member_value_ after it is
  // freed.
  co_await TakesReference(member_value_).OnRunner(other_task_runner_);
}

As such, I decided it was safer to require binding the arguments with base::Bind*(), which requires the programmer to be explicit about (and thus hopefully think about) lifetimes and ownership. One situation that could be safe without base::Bind*() would be switching to a different runner that runs tasks on the same sequence (e.g., to run at a different priority), since that would still allow checking WeakPtrs and protect against data races. I could implement that if folks would like to see it.


¹ As mentioned previously, one can still get into trouble here if TakesReference() stores the reference in a local object that then accesses the referenced value in its destructor, since destructors in the coroutine state are run when a task is canceled. Hopefully this isn't often a problem in practice.
 

On Mon, 3 Feb 2025 at 04:36, Erik Jensen <rkj...@chromium.org> wrote:

On Wed, Jan 29, 2025 at 4:43 AM Hans Wennborg <ha...@chromium.org> wrote:


On Tue, Jan 28, 2025 at 12:26 PM Erik Jensen <rkj...@chromium.org> wrote:
I don't know that I'd nearly qualify as an "expert", but I would enjoy being a part of that if it comes to fruition.

In the mean time, here's my draft prototype for implementing coroutines in Chromium: https://chromium-review.googlesource.com/c/chromium/src/+/6205666 (There are surely style, et cetera issues. I was mostly trying to get my thoughts down.)

I ended up changing my approach from what I initially sketched, since the initial sketch was meant to be something that could be used just in //remoting, while the prototype instead tries to integrate into //base to be more widely usable. The basic concept is "posting a coroutine task should be as similar to posting a regular task as possible." Specifically:
  • BindOnce() must be used to bind a coroutine's arguments to it prior to posting.
  • A coroutine OnceCallback can be posted as a task by passing it to a TaskRunner's PostTask() method. (This required updating a few sites that take a reference to PostTask() to explicitly select the desired overload. The alternative would be to have a distinctly named PostCoroutine() or some such.)
  • When a coroutine is passed to PostTaskAndReply(), the reply callback will be invoked when the coroutine has fully executed.
  • All of the code within a posted coroutine task runs on the same runner. If the coroutine awaits on something from a different thread / sequence / et cetera, the implementation ensures that it is still resumed on the expected sequence. (This is in contrast to many implementations, where the coroutine must be prepared to resume on any arbitrary thread.) This allows sequence-bound and thread-affine objects to be used within a coroutine.
    • However, the coroutine may be interleaved with other tasks on the same runner when suspending via co_await, and it may resume on a different physical thread (unless it was posted to a SingleThreadTaskRunner).
  • To do work on a different runner a new task must be posted using that runner.
    • I added PostTaskAndAwait() and PostTaskAndAwaitResult() methods (that work with both coroutine and regular tasks) to make it easy to co_await such a task.
  • Like with regular tasks, if the a task posted to PostTaskAndReply() is canceled, the reply callback will not be invoked.
    • Unlike with regular tasks, a coroutine task can be can canceled at a suspend point after it has started execution. (Or manually canceled by awaiting a special CancelTask value.)
    • If a task posted to PostTaskAndAwait() is canceled, the implementation makes a best-effort attempt to cancel the caller as well to prevent the waiting coroutine from leaking.
Handling WeakPtrs is a little tricky, since a WeakPtr might be invalidated by a interleaved task any time the coroutine suspends. Manually rechecking WeakPtrs after every resume isn't tenable, as the currently-executing coroutine might not even know the pointer / reference it was passed was derived from a WeakPtr, let alone have access to it. This is especially true for methods, which can only be passed their receiver as a raw pointer, and being able to co_await another method in the same class is pretty important for usability.

My solution is to allow coroutines to create a scoped guard declaring the use of a given WeakPtr. The implementation keeps a list of all such guards that are currently live on the coroutine stack, and will automatically check them all each time the task resumes. If any guarded WeakPtrs are invalid, the implementation will immediately cancel the task instead of resuming execution of the coroutine. When a WeakPtr is bound as the receiver for a coroutine method, BindOnce() will automatically ensure that such a guard exists for the lifetime of the bound coroutine's execution.

Like with a regular task, a coroutine task will be destroyed on the appropriate runner if it is canceled. However, since the coroutine task may have already started execution, there's more state to destroy than just the bound arguments in a regular task, and thus more of a chance that a destructor in the coroutine stack might try to access a pointer derived from a now-invalid WeakPtr. A coroutine needs to take care that, for objects that live across a suspend point, their destructors don't access any pointers or references that are not guaranteed to point to objects also on the coroutine stack. The alternative would be to leak any canceled coroutine instead of destroying it, but that seems undesirable.

There is one major caveat with my prototype: it currently crashes if the posted coroutine never actually suspends. I'm pretty sure the crash is due to a miscompilation of ExecuteInTaskImpl, but please let me know if there's a bug in that function that I'm missing. The resulting assembly looks like this:

Please file a bug in the Tools > LLVM component with a reproducer if you believe this is a miscompile.
 

0x5555593d887c  <+ 2172>        48 8b bd c0 fe ff ff     mov    rdi,QWORD PTR [rbp-0x140]
// Call to delete for the coroutine state (sample value for $rdi: 0x964000f0000)
0x5555593d8883  <+ 2179>        e8 38 12 44 00           call   0x555559819ac0 <_ZdlPv>
0x5555593d8888  <+ 2184>        8b 85 c4 fd ff ff        mov    eax,DWORD PTR [rbp-0x23c]
0x5555593d888e  <+ 2190>        85 c0                    test   eax,eax
0x5555593d8890  <+ 2192>        74 04                    je     0x5555593d8896
0x5555593d8892  <+ 2194>        eb 00                    jmp    0x5555593d8894
0x5555593d8894  <+ 2196>        eb 02                    jmp    0x5555593d8898
0x5555593d8896  <+ 2198>        eb 00                    jmp    0x5555593d8898
0x5555593d8898  <+ 2200>        eb 00                    jmp    0x5555593d889a
0x5555593d889a  <+ 2202>        eb 00                    jmp    0x5555593d889c
// Calls to destructors for copies of |task| and |current_runner| within the already-deleted coroutine state
0x5555593d889c  <+ 2204>        48 8b bd 48 fe ff ff     mov    rdi,QWORD PTR [rbp-0x1b8]
// $rdi here is 0x964000f00d8, within the just-freed allocation.
// Crashes because that memory has been overwritten with 0xcdcdcdcdcdcdcdcd.
0x5555593d88a3  <+ 2211>        e8 d8 79 f8 fd           call   0x555557360280 <_ZN13scoped_refptrIN4base10TaskRunnerEED2Ev>
0x5555593d88a8  <+ 2216>        48 8b bd 40 fe ff ff     mov    rdi,QWORD PTR [rbp-0x1c0]
0x5555593d88af  <+ 2223>        e8 dc e4 f6 fd           call   0x555557346d90 <_ZN4base12OnceCallbackIFNS_5AsyncIvEEvEED2Ev>
0x5555593d88b4  <+ 2228>        48 81 c4 40 02 00 00     add    rsp,0x240
0x5555593d88bb  <+ 2235>        5d                       pop    rbp
0x5555593d88bc  <+ 2236>        c3                       ret

If I uncomment line 219, suspending ExecuteInTaskImpl never to be resumed (leaking its state), then all the tests pass, but obviously that's not a tenable solution.

On Friday, January 10, 2025 at 1:49:13 PM UTC-8 Peter Kasting wrote:
I reached out to ATLs about how to do this The Right Way, hopefully I'll have an update soon on how we can figure this all out. 

It will likely look like a separate "expert committee" to make an initial investigation and recommendation around API shape, so if anyone specifically wants to be part of that -- or knows someone else who should -- let me know. There's a few people like gab and fdoray that I know should be there, but assume I'll miss even obvious choices and speak up :)

PK

On Thu, Jan 9, 2025, 2:04 AM Alex Clarke <alexc...@google.com> wrote:
FWIW with a suitable library in front of them, coroutines in C++ are really quite nice.  You can write code in a similar vein to JS or Kotlin style. I've got a side project where I've used this extensively. 

On Wed, 18 Dec 2024 at 18:50, 'Etienne Pierre-doray' via cxx <c...@chromium.org> wrote:
 Are there any particular classes (in //remoting or elsewhere) that might be good
You'll find some examples in http://bit.ly/cpp-promises

On Wed, Dec 18, 2024 at 1:35 PM Peter Kasting <pkas...@chromium.org> wrote:
Media code has a ton of several-levels-deep callbacks and continuations, and would probably be a place to refactor if we had coroutines.

PK

On Wed, Dec 18, 2024, 6:04 AM Kevin Smith <ksm...@brave.com> wrote:
I'm also experimenting with a co_await-enabling prototype and I'm currently looking for code that would benefit from co_await. Are there any particular classes (in //remoting or elsewhere) that might be good candidates?

Thanks!

On Friday, December 13, 2024 at 11:02:57 AM UTC-5 pkas...@chromium.org wrote:
On Thursday, December 12, 2024 at 7:10:17 PM UTC-8 Erik Jensen wrote:
Are you familiar with Google's internal coroutines code?

I have no experience or familiarity with C++ in google3 whatsoever.

OK. jacobsa@ is the relevant owner, and wrote the initial k3::Co library that I believe is now renamed c9::. He gave a good talk on coroutine implementation choices: https://www.youtube.com/watch?v=k-A12dpMYHo -- if you want to implement something, I would definitely watch.

 Are you familiar with my coroutines prototype?

I don't see it linked from https://crbug.com/40251667. Where would I find it?

https://chromium-review.googlesource.com/c/chromium/src/+/4227153, but it isn't really the right direction to go. It's a direct port of the Dec. 31 2022 version of the k3 code, so it's not aware of the task scheduler at all. The bug you linked has my comment with thoughts on a better design direction (which is more like what you're doing here).

You didn't mention the promises work, I assume you haven't seen it. There's a lot of overlap between that and what coroutines could do. But let's leave it aside for the moment.

To be clear, I don't think I have the context or capacity to design or implement a new task system in Chromium. While Chrome Remote Desktop lives in-tree and uses the task system from base, it is surely not representative of how that task system is used in the browser. It's also a very small team, so it would be challenging for us to invest the resources needed for a Chromium-wide endeavor.

We definitely don't want to reinvent the whole task system.  Being able to interface with the existing one is the right direction. I do think we need to have an idea of the long-term direction -- relevant base/ OWNERS like gab@ and I should be on the hook to help you with that, it shouldn't be on your shoulders.
 
The implementation of what I have in mind should be pretty straightforward, so maybe the best next step would be to post a CL that “‘merely’ provides a tool to rewrite laboriously broken-apart chains of execution, completion callback arguments, etc. as straight-line coroutine code” (as mentioned in the bug), and then have something concrete to discuss whether it would be okay in //remoting, be desired as an intermediate option for all of Chromium, or is fundamentally flawed.

Yeah, that sounds like a good route. A minimal working prototype is always easiest to think about.

PK

--
You received this message because you are subscribed to the Google Groups "cxx" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cxx+uns...@chromium.org.
To view this discussion visit https://groups.google.com/a/chromium.org/d/msgid/cxx/CAAHOzFAVDGMoOTaUeFNfk%3DBoMsxeWCA%3DqAnXGV82rmbvVmK5bg%40mail.gmail.com.

--
You received this message because you are subscribed to the Google Groups "cxx" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cxx+uns...@chromium.org.
To view this discussion visit https://groups.google.com/a/chromium.org/d/msgid/cxx/CALoDvsa4Mm14T2r7USfMmVhxd%3DHmPzkCPbWm_33Da0VOwCZvkQ%40mail.gmail.com.

--
You received this message because you are subscribed to the Google Groups "cxx" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cxx+uns...@chromium.org.
To view this discussion visit https://groups.google.com/a/chromium.org/d/msgid/cxx/0fd915dd-9f46-4152-b4bd-b278e00fee79n%40chromium.org.

Erik Jensen

unread,
Mar 13, 2025, 6:46:42 PMMar 13
to cxx, Erik Jensen, Hans Wennborg, cxx, Peter Kasting, etie...@google.com, ksm...@brave.com, Alex Clarke, schedu...@chromium.org
Thanks to some great work by hans@, the Clang miscompilation is now fixed. I've rebased the prototype, and all of the tests now pass.

Victor Vianna

unread,
May 20, 2025, 1:38:28 PMMay 20
to cxx, rkj...@chromium.org, ha...@chromium.org, cxx, Peter Kasting, Etienne Pierre-doray, ksm...@brave.com, Alex Clarke, schedu...@chromium.org
Drive-by: coroutines just got allowed in google internal code

Hans Wennborg

unread,
May 21, 2025, 2:45:50 AMMay 21
to Victor Vianna, cxx, rkj...@chromium.org, Peter Kasting, Etienne Pierre-doray, ksm...@brave.com, Alex Clarke, schedu...@chromium.org
LLVM/Clang does not yet support coroutines for 32-bit Windows (it has some support, but also some missing), so that is something we'd have to fix if Chromium decides to adopt coroutines.
Reply all
Reply to author
Forward
0 new messages