Skip to content

Conversation

@oldnewthing
Copy link
Member

@oldnewthing oldnewthing commented Mar 5, 2021

Clients of IAsync...WithProgress are permitted to call GetResults() before the operation has completed, allowing them to observe partial results. This behavior is necessary because the Progress type must be a value type, but the operation may want to report reference types in their progress reports. The way to do that is to report the reference type as the GetResults().

The coroutine itself can use the set_result() method on the progress token to report a result prior to completion, and can set a new intermediate result as often as desired prior to the completion of the coroutine. The value passed to co_return acts as the final result of the coroutine. If set_result() is never called, then the intermediate result is an empty result (null for reference types, default-initialized for value types).

In practice, the result of the coroutine is usually a reference type, and the common pattern for coroutines that want to report a reference type as progress is to create the result object, pass it to set_result() at the start of the coroutine, update the result object as the coroutine progresses, and (redundantly) pass the same result object to co_return when the coroutine completes.

IAsyncOperationWithProgress<Muffin, double> BakeMuffinAsync()
{
    auto progress = co_await get_progress_token();

    Muffin muffin;
    Muffin.Diameter(5);
    progress.set_result(muffin);
    progress(0.0);

    muffin.Diameter(5.2);
    progress(0.5);

    muffin.Diameter(6);
    progress(1.0);

    co_return muffin;
}

Since clients of ...WithProgress are permitted to call GetResults() prior to completion, this introduces a few new wrinkles.

  1. We need to use the lock to ensure a client doesn't try to read a partial result at the same time we are writing a new one.
  2. The GetResults() method must return a copy of the result. This is true even if the coroutine has completed, because a progress handler might go off and do some asynchronous work, and then come back and ask for the intermediate result. If GetResults() on a completed coroutine consumes the result, then the async progress handler and the co_await will compete to retrieve the completed results, and somebody will lose.

As a result, ...WithProgress coroutines are slightly more expensive than non-progress coroutines due to the extra locking and copying. Fortunately, it's mostly pay-for-play. If a coroutine never calls set_result(), and the client never calls GetResults() from its progress handler, then there is no new lock contention. The final copy is unavoidable, but fortunately, the result is usually a reference type, so you just pay an extra AddRef/Release pair.

Clients of IAsync...WithProgress are permitted to call GetResults()
before the operation has completed, allowing them to observe partial
results. This behavior is necessary because the Progress type must
be a value type, but the operation may want to report reference types
in their progress reports. The way to do that is to report the reference
type as the GetResults().

The coroutine itself can use the `set_result()` method on the progress
token to report a result prior to completion, and can update that result
as often as desired prior to the completion of the coroutine. The value
passed to `co_return` acts as the final result of the coroutine. If
`set_result()` is never called, then the intermediate result is an empty
result (null for reference types, default-initialized for value types).

In practice, the result of the coroutine is usually a reference type,
and the common pattern is to create the result object, pass it to
`set_result()` at the start of the coroutine, update the result object
as the coroutine progresses, and (redundantly) pass the same result object
to `co_return` when the coroutine completes.

```cpp
IAsyncOperationWithProgress<BakingResult, double> BakeMuffinAsync()
{
    auto progress = co_await get_progress_token();

    BakingResult result;
    result.Status(BakingStatus::WarmingUp);
    progress.set_result(result);
    progress(0.0);

    result.Status(BakingStatus::Baking);
    progress(0.2);

    result.Muffin(Muffin());
    result.Status(BakingStatus::Success);
    progress(1.0);

    co_return result;
}
```

Since clients of ...WithProgress are permitted to call GetResults()
prior to completion, this introduces a few new wrinkles.

1. We need to use the lock to ensure a client doesn't try to
   read a partial result at the same time we are writing a new one.
2. The GetResults() method must return a copy of the result.
   This is true even if the coroutine has completed, because
   a progress handler might go off and do some asynchronous work,
   and then come back and ask for the intermediate result. If the
   GetResults() method moves the results of a completed coroutine,
   then the async progress handler and the co_await will compete
   to retrieve the completed results, and somebody will lose.

As a result, ...WithProgress coroutines are slightly more expensive
than non-progress coroutines due to the extra locking and copying.
Fortunately, it's mostly pay-for-play. If a coroutine never calls
`set_result()`, and the client never calls `GetResults()` from its
progress handler, then there is no new lock contention. The final
copy is unavoidable, but fortunately, the result is usually a reference
type, so you just pay an extra AddRef/Release pair.
@oldnewthing oldnewthing requested review from jonwis and kennykerr March 5, 2021 07:06
Copy link
Collaborator

@kennykerr kennykerr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good - thanks!

Every call to `atomic.load()` goes to memory.
Cache the value to remove extra memory accesses.

Split more of the implementation of `GetResult()`
since there are optimizations available to the
...WithProgress cases, since the `illegal_method_call`
case can never happen.
@kennykerr kennykerr merged commit e37bf8a into master Mar 8, 2021
@kennykerr kennykerr deleted the oldnewthing/progress-early-results branch March 8, 2021 13:56
This was referenced Mar 15, 2021
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

Successfully merging this pull request may close these issues.

4 participants