Skip to content

Add documentation for synchronous access to asynchronous operations #47735

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs/csharp/asynchronous-programming/async-scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,71 @@ Avoid writing code that depends on the state of global objects or the execution

A recommended goal is to achieve complete or near-complete [Referential Transparency](https://en.wikipedia.org/wiki/Referential_transparency) in your code. This approach results in a predictable, testable, and maintainable codebase.

### Synchronous access to asynchronous operations

In rare scenarios, you might need to block on asynchronous operations when the `await` keyword isn't available throughout your call stack. This situation commonly occurs in legacy codebases or when integrating asynchronous methods into synchronous APIs that can't be changed.
Copy link
Contributor

Choose a reason for hiding this comment

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

So is it rare or is it common? :)


> [!WARNING]
> Synchronous blocking on asynchronous operations can lead to deadlocks and should be avoided whenever possible. The preferred solution is to use `async`/`await` throughout your call stack.

When you must block synchronously on a `Task`, here are the available approaches, listed from most to least preferred:

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we list the approaches here with links to the headings?

#### Use GetAwaiter().GetResult()

The `GetAwaiter().GetResult()` pattern is generally the preferred approach when you must block synchronously:

```csharp
// When you cannot use await
Task<string> task = GetDataAsync();
string result = task.GetAwaiter().GetResult();
```

This approach:

- Preserves the original exception without wrapping it in an `AggregateException`
- Blocks the current thread until the task completes
- Still carries deadlock risk if not used carefully
Comment on lines +212 to +214
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- Preserves the original exception without wrapping it in an `AggregateException`
- Blocks the current thread until the task completes
- Still carries deadlock risk if not used carefully
- Preserves the original exception without wrapping it in an `AggregateException`.
- Blocks the current thread until the task completes.
- Still carries deadlock risk if not used carefully.


#### Use Task.Run for complex scenarios

For complex scenarios where you need to isolate the asynchronous work:

```csharp
// Offload to thread pool to avoid context deadlocks
string result = Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();
```

This pattern:

- Executes the asynchronous method on a thread pool thread
- Can help avoid some deadlock scenarios
- Adds overhead by scheduling work to the thread pool
Comment on lines +227 to +229
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- Executes the asynchronous method on a thread pool thread
- Can help avoid some deadlock scenarios
- Adds overhead by scheduling work to the thread pool
- Executes the asynchronous method on a thread pool thread.
- Can help avoid some deadlock scenarios.
- Adds overhead by scheduling work to the thread pool.


#### Avoid Wait() and Result
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
#### Avoid Wait() and Result
#### Use Wait() and Result

Copy link
Contributor

Choose a reason for hiding this comment

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

Since it's listed as one of the "approaches", albeit the least preferred one, it seems weird to start the heading with "Avoid".


These blocking approaches are discouraged:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
These blocking approaches are discouraged:
You can use a blocking approach by calling <xref:System.Threading.Tasks.Task.Wait> and <xref:System.Threading.Tasks.Task`1.Result>. However, this approach is discouraged because it wraps exceptions in <xref:System.AggregateException>.

Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably update the instructions file to say that it's better to put info in the Markdown than in code comments (since it's not localized).


```csharp
// Discouraged: Wraps exceptions in AggregateException
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Discouraged: Wraps exceptions in AggregateException

Task<string> task = GetDataAsync();
task.Wait();
string result = task.Result;
```

Problems with `Wait()` and `Result`:

- Exceptions are wrapped in `AggregateException`, making error handling more complex
- Higher deadlock risk
- Less clear intent in code
Comment on lines +244 to +246
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- Exceptions are wrapped in `AggregateException`, making error handling more complex
- Higher deadlock risk
- Less clear intent in code
- Exceptions are wrapped in `AggregateException`, making error handling more complex.
- Higher deadlock risk.
- Less clear intent in code.


#### Additional considerations

- **Deadlock prevention**: Be especially careful in UI applications or when using a synchronization context
- **Performance impact**: Blocking threads reduces scalability
- **Exception handling**: Test error scenarios carefully as exception behavior differs between patterns
Comment on lines +250 to +252
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- **Deadlock prevention**: Be especially careful in UI applications or when using a synchronization context
- **Performance impact**: Blocking threads reduces scalability
- **Exception handling**: Test error scenarios carefully as exception behavior differs between patterns
- Deadlock prevention: Be especially careful in UI applications or when using a synchronization context.
- Performance impact: Blocking threads reduces scalability.
- Exception handling: Test error scenarios carefully as exception behavior differs between patterns.


For more detailed guidance on the challenges and considerations of synchronous wrappers for asynchronous methods, see [Should I expose synchronous wrappers for asynchronous methods?](https://devblogs.microsoft.com/pfxteam/should-i-expose-synchronous-wrappers-for-asynchronous-methods/).

## Review the complete example

The following code represents the complete example, which is available in the *Program.cs* example file.
Expand Down