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

make TryAsyncContinuation async #63

Closed
wants to merge 1 commit into from

Conversation

caleblloyd
Copy link
Contributor

  • Helps increase efficiency for Async calls by removing Synchronous calls in Async Code
  • Helps increase efficiency for Async and Sync calls by reducing need for context switching

I used a method that retrieved 10 blog posts from the Performance Test database. I logged every call to GetAwaiter().GetResult() to see what Managed Thread ID the calls were running on.

Async calls to GetAwaiter().GetResult() Before Change:

Thread ID: 4 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 3 Kicking off task
Thread ID: 3 Kicking off task
Thread ID: 3 Kicking off task
Thread ID: 3 Kicking off task
Thread ID: 3 Kicking off task

Async calls to GetAwaiter().GetResult() After Change:

Thread ID: 5 Kicking off task (this happens in MysqlDataReader.Dispose when it calls NextResult)

Sync calls to GetAwaiter().GetResult() Before Change:

Thread ID: 5 Kicking off task
Thread ID: 4 Kicking off task
Thread ID: 10 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 10 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 4 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 10 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 4 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 10 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task

Sync calls to call to GetAwaiter().GetResult() After Change:

Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task
Thread ID: 5 Kicking off task

@bgrainger
Copy link
Member

This PR is replacing a hand-written continuation with a compiler-generated one that is less efficient due to the async state machine. It also allocates extra (unnecessary) Tasks.

task.ContinueWith(TryAsyncContinuation) schedules TryAsyncContinuation to run when the task is completed. There is no "blocking" because the antecedent task has finished, so GetAwaiter().GetResult() returns immediately (or throws).

Adding await task.ConfigureAwait(false) to the body of TryAsyncContinuation sets up an almost identical continuation, except it requires extra compiled-generated code and temporary objects. Behind the scenes, the compiler-generated code calls GetResult() on the task's awaiter.

Your change does suggest an approach that I haven't tested yet, though: using GetAwaiter().OnCompleted() to schedule the continuation instead of ContinueWith. However, the documentation explicitly says "This API supports the product infrastructure and is not intended to be used directly from your code." so it seems unwise. I haven't checked the implementation, but I'd be very surprised if OnCompleted wasn't implemented in terms of ContinueWith, though.

@bgrainger
Copy link
Member

but I'd be very surprised if OnCompleted wasn't implemented in terms of ContinueWith, though

OnCompleted calls SetContinuationForAwait which calls AddTaskContinuation; ContinueWith calls ContinueWithCore which also calls AddTaskContinuation. So the two should be roughly equivalent in terms of overhead.

@caleblloyd
Copy link
Contributor Author

Thanks for the information and references.

There is no "blocking" because the antecedent task has finished

This makes sense.

Behind the scenes, the compiler-generated code calls GetResult() on the task's awaiter

Good to know. So it's running on a different thread anyways, I just can't log the result because it's compiler generated. This makes total sense. In both of the Sync Before/After examples there are 14 calls on Thread ID: 5. The only difference is I've masked logging.

I'll read up more on these articles, it's great to know what's actually going on in the compiler!

@caleblloyd caleblloyd closed this Sep 25, 2016
@caleblloyd caleblloyd deleted the f_continuation branch October 12, 2016 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants