I have a custom awaitable type and the problem is that the continuation resumes on a different thread, which causes problems in UIs such as WinForms/WPF/MVC/etc:
private MyAwaitable awaitable;
private async void buttonStart_Click(object sender, EventArgs e)
{
awaitable = new MyAwaitable(false);
progressBar1.Visible = true;
// A regular Task can marshal the execution back to the UI thread
// Here ConfigureAwait is not available and I don't know how to control the flow
var result = await awaitable;
// As a result, here comes the usual "Cross-thread operation not valid" exception
// A [Begin]Invoke could help but regular Tasks also can handle this situation
progressBar1.Visible = false;
}
private void buttonStop_Click(object sender, EventArgs e) => awaitable.Finish();
Here is the MyAwaitable
class:
public class MyAwaitable
{
private volatile bool finished;
public bool IsFinished => finished;
public MyAwaitable(bool finished) => this.finished = finished;
public void Finish() => finished = true;
public MyAwaiter GetAwaiter() => new MyAwaiter(this);
}
And the problematic custom awaiter:
public class MyAwaiter : INotifyCompletion
{
private readonly MyAwaitable awaitable;
private readonly SynchronizationContext capturedContext = SynchronizationContext.Current;
public MyAwaiter(MyAwaitable awaitable) => this.awaitable = awaitable;
public bool IsCompleted => awaitable.IsFinished;
public int GetResult()
{
var wait = new SpinWait();
while (!awaitable.IsFinished)
wait.SpinOnce();
return new Random().Next();
}
public void OnCompleted(Action continuation)
{
// continuation(); // This would block the UI thread
// Task constructor + Start was suggested by the references I saw,
// Results with Task.Run/Task.Factory.StartNew are similar.
var task = new Task(continuation, TaskCreationOptions.LongRunning);
// If executed from a WinForms app, we have a WinFormsSyncContext here,
// which is promising, still, it does not solve the problem.
if (capturedContext != null)
capturedContext.Post(state => task.Start(), null);
else
task.Start();
}
}
I suspect that my OnCompleted
implementation is not quite correct.
I tried to dig into the ConfiguredTaskAwaiter
returned by the Task.ConfigureAwait(bool).GetAwaiter()
method and could see that the black magic happens in a SynchronizationContextAwaitTaskContinuation
class but that is an internal one, along with lot of other internally used types. Is there a way to refactor my OnCompleted
implementation to work as expected?
Update: Note to downvoters: I know I do improper things in OnCompleted
, that's why I ask. If you have concerns about quality (or anything else) please leave a comment and help me to improve the question so I also can help you to highlight the problem better. Thanks.
Note 2: I know I could use a workaround with a TaskCompletionSource<TResult>
and its regular Task<TResult>
result but I would like to understand the background. This is the only motivation. Pure curiosity.
Update 2: Notable references I investigated:
How awaiter works:
Some implementations:
- https://marcinotorowski.com/2018/03/13/tap-await-anything-not-only-tasks/ - wrong, blocks caller thread until
GetResult
returns - https://blogs.msdn.microsoft.com/pfxteam/2011/01/13/await-anything - just calls other awaiters
- https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-2-awaitable-awaiter-pattern - similar to mine, has the same issue