dotnet / dotnet/runtime

Optimized way/API to efficiently "yield to thread pool or no-op if already on it"

Open
#130,434 3 comments 0 reactions 0 assignees View on GitHub
area-System.Threading.Tasks tenet-performance
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

> Follow-up from https://github.com/dotnet/runtime/pull/130170#issuecomment-4905488174

Opening this issue to discuss the "yield to thread pool or no-op if already on it" semantics, specifically:
- What's the most efficient way to do it (taking runtime async into account too, cc. @jakobbotsch)
- Whether it'd make sense to have a dedicated API for this in the BCL (if so, happy to open a separate proposal)

We do this extensively in the Microsoft Store, and especially for GUI apps it's quite useful. Very often people end up making the UI sluggish because they accidentally start UI-driven synchronous CPU-bound work on the UI thread. Using an API like this makes it easy to just offload to a thread pool thread instead (eventually switching back to the UI thread if needed through some other dispatcher API, out of scope here).

Here's the simplest possible implementation for this that I have:

```csharp
public static class TaskSchedulerExtensions
{
extension(TaskScheduler)
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TaskSchedulerYieldAwaitable Yield()
{
return default;
}
}

[EditorBrowsable(EditorBrowsableState.Never)]
public readonly struct TaskSchedulerYieldAwaitable
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Awaiter GetAwaiter()
{
return default;
}

[EditorBrowsable(EditorBrowsableState.Never)]
public readonly struct Awaiter : ICriticalNotifyCompletion
{
private static readonly WaitCallback OnCompletedCallback = static state => Unsafe.As(state!)();

public bool IsCompleted
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Thread.CurrentThread.IsThreadPoolThread;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void GetResult()
{
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void OnCompleted(Action continuation)
{
ThreadPool.QueueUserWorkItem(OnCompletedCallback, continuation);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UnsafeOnCompleted(Action continuation)
{
ThreadPool.UnsafeQueueUserWorkItem(OnCompletedCallback, continuation);
}
}
}
}
```

So you can just do:

```csharp
await TaskScheduler.Yield();
```

And you're on the thread pool, or if you already were, then this call is a no-op.

Questions:
- Is this the best and most efficient way to do this, or could this be optimized
- Will this be de-optimized with runtime async or can it still be efficient?
- Should we have a built-in API in the BCL for this?

cc. @VSadov

> [!NOTE]
> Not using `await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);` because that _always_ yields.

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the proposed TaskSchedulerExtensions and TaskSchedulerYieldAwaitable implementation, then compare its behavior with ConfigureAwaitOptions.ForceYielding and the runtime async considerations described in the issue. Done means reaching a decision about the efficient implementation and whether a built-in BCL API should be proposed.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.