-2

The Task.Yield method "creates an awaitable task that asynchronously yields back to the current context when awaited." I am searching for something similar that should guarantee that any code that follows will run on a ThreadPool thread. I know that I could achieve this be enclosing all the following code in a Task.Run, but I am searching for an inline solution that doesn't create an inner scope.

private async void Button1_Click(object sender, EventArgs e)
{
    await Task.Yield(); // Doesn't do what I want
    // Code that should run on the ThreadPool
}

private async void Button1_Click(object sender, EventArgs e)
{
    // Does what I want, but I am not happy with the added indentation and scope
    await Task.Run(() => 
    {
        // Code that should run on the ThreadPool
    });
}

The best I can think of is to use the Task.Run with an empty delegate, and configure the awaiting to not capture the synchronization context:

private async void Button1_Click(object sender, EventArgs e)
{
    // Probably does what I want, but it looks ugly
    await Task.Run(() => { }).ConfigureAwait(continueOnCapturedContext: false);
    // Code that should run on the ThreadPool
}

This looks like an obscure line of code, and I am not sure that expresses well enough the intent of being there. I am also not sure if it provides the guarantees I want. Is there any other solution?

Btw this question is inspired by a Marc Gravell's answer to a related question.


Update: I should give a more specific reason about why using the standard await Task.Run(() => is not ideal in my case. I have some code that should run on the ThreadPool or not, depending on some condition. So a Task.Yield equivalent would allow me to do this:

private bool _executeOnThreadPool;

private async void Button1_Click(object sender, EventArgs e)
{
    if (_executeOnThreadPool) await SwitchToTheThreadPool();
    // Code that should run on the ThreadPool or the UI thread
}

I can't do the same thing with Task.Run without code duplication, or without adding lambdas and indirection.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • `await Task.Delay(0).ConfigureAwait(false);` ? Also I would say that second option is the clearest one. – Guru Stron Jun 29 '20 at 15:31
  • @GuruStron AFAIK the `Task.Delay(0)` returns a completed task, so the following code will continue on the UI thread. – Theodor Zoulias Jun 29 '20 at 15:33
  • Yep. seems so. Then Task.Delay(1) =))) – Guru Stron Jun 29 '20 at 15:34
  • @GuruStron I am not very happy with this either, because it introduces a tiny but unneeded artificial delay. – Theodor Zoulias Jun 29 '20 at 15:36
  • You could format your code [like this](https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgJgIwFgBQyAMABMugKwDcOyAzEagQMIEDeOBbRNyAHEQCwEAxAPZCAFAEoCAXgB8RAJxEAbADoASgFcAdqInSZrdi2yG2AX3EVsZoA) and avoid the indentation >: – canton7 Jun 29 '20 at 15:37
  • @canton7 this formatting is nifty, thanks. But I am searching for a true inline solution, based on the magical `await` keyword, with no extra scope/closure added to the mix. :-) – Theodor Zoulias Jun 29 '20 at 15:43
  • Raymond Chen's blogged about something I'm sure – canton7 Jun 29 '20 at 15:46
  • Somewhat related: [Why was “SwitchTo” removed from Async CTP / Release?](https://stackoverflow.com/questions/15363413/why-was-switchto-removed-from-async-ctp-release/) – Theodor Zoulias Sep 28 '20 at 09:55

1 Answers1

0

Raymond Chen posted about this in his blog The Old New Thing in the post C++/WinRT envy: Bringing thread switching tasks to C# (WPF and WinForms edition).

Reproduced here in case the source goes down:

using System;
using System.Runtime.CompilerServices;
using System.Threading;          // For ThreadPool
using System.Windows.Forms;      // For Windows Forms
using System.Windows.Threading;  // For WPF

// For WPF
struct DispatcherThreadSwitcher : INotifyCompletion
{
    internal DispatcherThreadSwitcher(Dispatcher dispatcher) =>
        this.dispatcher = dispatcher;
    public DispatcherThreadSwitcher GetAwaiter() => this;
    public bool IsCompleted => dispatcher.CheckAccess();
    public void GetResult() { }
    public void OnCompleted(Action continuation) =>
        dispatcher.BeginInvoke(continuation);
    Dispatcher dispatcher;
}

// For Windows Forms
struct ControlThreadSwitcher : INotifyCompletion
{
    internal ControlThreadSwitcher(Control control) =>
        this.control = control;
    public ControlThreadSwitcher GetAwaiter() => this;
    public bool IsCompleted => !control.InvokeRequired;
    public void GetResult() { }
    public void OnCompleted(Action continuation) =>
        control.BeginInvoke(continuation);
    Control control;
}

// For both WPF and Windows Forms
struct ThreadPoolThreadSwitcher : INotifyCompletion
{
    public ThreadPoolThreadSwitcher GetAwaiter() => this;
    public bool IsCompleted =>
        SynchronizationContext.Current == null;
    public void GetResult() { }
    public void OnCompleted(Action continuation) =>
        ThreadPool.QueueUserWorkItem(_ => continuation());
}

class ThreadSwitcher
{
    // For WPF
    static public DispatcherThreadSwitcher ResumeForegroundAsync(
        Dispatcher dispatcher) =>
        new DispatcherThreadSwitcher(dispatcher);

    // For Windows Forms
    static public ControlThreadSwitcher ResumeForegroundAsync(
        Control control) =>
        new ControlThreadSwitcher(control);

    // For both WPF and Windows Forms
    static public ThreadPoolThreadSwitcher ResumeBackgroundAsync() =>
         new ThreadPoolThreadSwitcher();
}

This lets you do:

await ThreadSwitcher.ResumeBackgroundAsync();

I'd be wary about actually doing this in a shared codebase however: it's not particularly idiomatic, whereas Task.Run is very clear.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • 1
    Thaks canton7, this is very helpful! I managed to achieve exactly the behavior I want by making an adaptation to the `ThreadPoolThreadSwitcher`s property `IsCompleted`. This is my version: `public bool IsCompleted => Thread.CurrentThread.IsThreadPoolThread;` – Theodor Zoulias Jun 29 '20 at 16:20
  • 1
    Yeah, checking the SynchronizationContext did seem a little weird: that indicates any old non-dispatcher thread, rather than a ThreadPool thread specifically. – canton7 Jun 29 '20 at 16:32