microsoft / cppwinrt

C++/WinRT
MIT License
1.64k stars 236 forks source link

resume_agile to allow coroutine to resume in any apartment #1356

Closed oldnewthing closed 11 months ago

oldnewthing commented 11 months ago

By default, co_await'ing a Windows Runtime asynchronous operation resumes in the same COM apartment. We provide a way to override this behavior and resume in any apartment by wrapping the asynchronous operation in resume_agile().

extern IAsyncAction Example();

// This resumes in the same apartment
co_await Example();

// This resumes in any apartment
co_await resume_agile(Example());

The guts of the change are in disconnect_aware_handler which accepts a preserve_context template parameter, defaulting to true. If false, then we bypass the resume_apartment() on completion. The false value is provided by the resume_agile() function.

To remove the resume_apartment_context from the disconnect_aware_handler in the case where we don't need it, I use EBO and make it a base class, so that it disappears when an empty ignore_apartment_context is used.

While I was there, I introduced movable_primitive<T> which provides RAII-like functionality to scalars like integers and pointers. This allows us to simplify disconnect_aware_handler and resume_apartment_context, which previously had to override all the copy and move operations in order to reset the scalar on move.

Also fix the await_adapter.cpp tests so they don't leak DispatcherQueues, by explicitly shutting down all the dispatcher queues we created. This required moving all the controllers to top-level so we can shut them down after the tests have completed.

Fixes: #1355

oldnewthing commented 11 months ago

Open issues:

Name Free Member
resume_agile co_await winrt::resume_agile(Something()); co_await Something().resume_agile();
resume_any_apartment co_await winrt::resume_any_apartment(Something()); co_await Something().resume_any_apartment();

Note that C# uses await Something().ConfigureAwait(false);, going for the "member function" column, though the name "ConfigureAwait" is quite bad.

Pros/cons:

jonwis commented 11 months ago

My vote is for free function. Our policy so far has to put these things as free functions so discoverability doesn't seem like a problem here ... resume_foreground, cancellation helpers, etc. If you want this, you know you need it, so you know where to find it. (I'm of two minds about monkeypatching via extension methods for the same reason.)

Can you capture it by move, so someone who says co_await resume_agile(ZorpAsync()) gets the best results?

I like resume_agile() as the naming.

oldnewthing commented 11 months ago

Can you capture it by move, so someone who says co_await resume_agile(ZorpAsync()) gets the best results?

I like this idea - it gives us the best of both worlds.

sylveon commented 11 months ago

await_adapter is not owning, it holds Async const&

oldnewthing commented 11 months ago

await_adapter is not owning, it holds Async const&

The difference is that up until now, await_adapter had been produced only by operator co_await, so you knew that the await_adapter was going to be awaited immediately, before the destruction of any temporaries.

This change introduces a way to obtain an await_adapter without awaiting it, namely by doing

auto oops = resume_agile(ZorpAsync());
co_await oops;
sylveon commented 11 months ago

That is a fair assessment, though one could already get an adapter without awaiting by calling the operator manually.

To make this work, await_adapter and all operators would need to be changed to take ownership, not just resume_agile.

oldnewthing commented 11 months ago

Somebody who invokes the operator manually auto oops = operator co_await(ZorpAsync()) knows that they are holding a live wire.