如何正确实现自定义等候者的OnCompleted方法?

时间:2018-07-17 07:14:40

标签: c# async-await

我有一个自定义的等待类型,问题是继续在不同的线程上继续进行,这会在WinForms / WPF / MVC / etc等UI中引起问题:

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();

这是MyAwaitable类:

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);
}

有问题的自定义侍者:

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();
    }
}

我怀疑我的OnCompleted实现不太正确。

我试图深入研究ConfiguredTaskAwaiter方法返回的Task.ConfigureAwait(bool).GetAwaiter(),发现黑魔法发生在SynchronizationContextAwaitTaskContinuation类中,但这是一个内部类,还有很多其他内部使用的类型。有没有一种方法可以重构我的OnCompleted实现以达到预期的效果?

更新投票者注意:我知道我在OnCompleted中做了不当的事情,这就是为什么要问这个问题。如果您对质量(或其他任何方面)有疑问,请发表评论并帮助我改善问题,以便我也可以帮助您更好地突出问题。谢谢。

注释2 :我知道我可以对TaskCompletionSource<TResult>及其常规Task<TResult>结果使用变通办法,但我想了解背景知识。 这是唯一的动机。纯粹的好奇心。

更新2 :我调查过的重要参考文献:

服务员的工作方式:

一些实现:

3 个答案:

答案 0 :(得分:4)

OnCompleted方法的MSDN解释是:

  

安排实例完成时调用的继续操作。

因此OnCompleted的两个实现都不是“正确的”,因为如果awaiter尚未完成,则awaitable不应在该调用期间执行传递的委托,但是注册awaitable完成后执行。

唯一不清楚的是,如果awaitable在调用方法时已经完成,该方法应该做什么(尽管在这种情况下编译器生成的代码不会调用它)-忽略连续委托或执行。根据{{​​1}}的实现,应该晚些(执行)。

当然,该规则也有例外(因此,“正确” 一词)。例如,Task特别地总是返回YieldAwaiter来强制调用它的IsCompleted == false方法,该方法立即在线程池上调度传递的委托。但是“通常”您不会那样做。

通常(与标准OnCompleted实现一样),Task将执行操作,提供结果,等待机制,并维护/执行延续。他们的awaitable通常是awaiters,持有对共享struct的引用(必要时还有继续选项),并将委托awaitableGetResult方法调用到共享的OnCompleted中,并且专门用于awaitable传递延续委托以及负责注册/执行它们的OnCompleted内部方法的选项。 “可配置” awaitable将仅保存共享的awaitable和选项,然后将其传递给创建的awaitable

由于在您的示例中,等待和结果由awaiter提供,所以awaiter可以简单地提供完成事件:

awaitable

,然后public class MyAwaitable { private volatile bool finished; public bool IsFinished => finished; public event Action Finished; public MyAwaitable(bool finished) => this.finished = finished; public void Finish() { if (finished) return; finished = true; Finished?.Invoke(); } public MyAwaiter GetAwaiter() => new MyAwaiter(this); } 个订阅:

awaiter

调用public class MyAwaiter : INotifyCompletion { private readonly MyAwaitable awaitable; private int result; public MyAwaiter(MyAwaitable awaitable) { this.awaitable = awaitable; if (IsCompleted) SetResult(); } public bool IsCompleted => awaitable.IsFinished; public int GetResult() { if (!IsCompleted) { var wait = new SpinWait(); while (!IsCompleted) wait.SpinOnce(); } return result; } public void OnCompleted(Action continuation) { if (IsCompleted) { continuation(); return; } var capturedContext = SynchronizationContext.Current; awaitable.Finished += () => { SetResult(); if (capturedContext != null) capturedContext.Post(_ => continuation(), null); else continuation(); }; } private void SetResult() { result = new Random().Next(); } } 时,首先检查是否完整。如果是,我们只执行传递的委托并返回。否则,我们将捕获同步上下文,订阅OnCompleted完成事件,然后在该事件内部通过捕获的同步上下文或直接执行操作。

同样,在现实生活中,awaitable应该执行真实的工作,提供结果并维护延续动作,而awaitable s应该仅注册延续动作,最终抽象出延续执行策略-直接通过捕获的同步上下文,通过线程池等。

答案 1 :(得分:0)

注意:最初,我在@IvanStoev给出正确答案后将此问题作为总结放在问题的结尾(非常感谢您的启发)。现在,我将该部分提取为真实答案。


因此,根据Ivan的回答,这里是一个简短的摘要,其中包含缺少的部分,我认为应该在文档中。下面的示例还模仿了ConfigureAwait的{​​{1}}行为。

1。测试应用

一个带有Task和3个ProgressBar控件的WinForms应用程序(也可以是其他单线程UI):一个按钮只是启动一个异步操作(和一个进度栏),其他按钮完成要么在UI线程中,要么在外部线程中。

Button

2。自定义等待类

与问题中的原始示例相反,此处的等待类本身包含延续,一旦执行完成,便会调用该延续。因此,侍应生可以要求安排继续执行的时间以供以后执行。

请注意,public partial class Form1 : Form { public Form1() { InitializeComponent(); progressBar.Style = ProgressBarStyle.Marquee; progressBar.Visible = false; } private MyAwaitable awaitable; private async void buttonStart_Click(object sender, EventArgs e) { awaitable = new MyAwaitable(); progressBar.Visible = true; var result = await awaitable; //.ConfigureAwait(false); from foreign thread this throws an exception progressBar.Visible = false; MessageBox.Show(result.ToString()); } private void buttonStopUIThread_Click(object sender, EventArgs e) => awaitable.Finish(new Random().Next()); private void buttonStopForeignThread_Click(object sender, EventArgs e) => Task.Run(() => awaitable.Finish(new Random().Next())); } ConfigureAwait基本相同-后者可以使用默认配置。

GetAwaiter

3。侍者

public class MyAwaitable { private volatile bool completed; private volatile int result; private Action continuation; public bool IsCompleted => completed; public int Result => RunToCompletionAndGetResult(); public MyAwaitable(int? result = null) { if (result.HasValue) { completed = true; this.result = result.Value; } } public void Finish(int result) { if (completed) return; completed = true; this.result = result; continuation?.Invoke(); } public MyAwaiter GetAwaiter() => ConfigureAwait(true); public MyAwaiter ConfigureAwait(bool captureContext) => new MyAwaiter(this, captureContext); internal void ScheduleContinuation(Action action) => continuation += action; internal int RunToCompletionAndGetResult() { var wait = new SpinWait(); while (!completed) wait.SpinOnce(); return result; } } 现在不执行延续(不同于我研究的示例),而是通过调用OnCompleted将其注册以供以后使用。

第二,请注意,现在等待者还有一个MyAwaitable.ScheduleContinuation方法,它只会返回自身。 GetAwaiter的用法需要使用它。

await myAwaitable.ConfigureAwait(bool)

答案 2 :(得分:-1)

这证明了延续在捕获的上下文中运行

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);
}

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()
    {
        SpinWait.SpinUntil(() => awaitable.IsFinished);
        return new Random().Next();
    }

    public void OnCompleted(Action continuation)
    {
        if (capturedContext != null) capturedContext.Post(state => continuation(), null);
        else continuation();
    }
}

public class MySynchronizationContext : SynchronizationContext
{
    public override void Post(SendOrPostCallback d, object state)
    {
        Console.WriteLine("Posted to synchronization context");
        d(state);
    }
}

class Program
{
    static async Task Main()
    {
        SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext());

        var awaitable = new MyAwaitable(false);

        var timer = new Timer(_ => awaitable.Finish(), null, 100, -1);

        var result = await awaitable;

        Console.WriteLine(result);
    }
}

输出:

Posted to synchronization context
124762545

但是您没有将延续发布到同步上下文中。

您要发布调度在另一个线程上继续执行的时间。

计划在同步上下文上运行,但连续本身却没有。因此就是你的问题。

您可以阅读this来了解其工作原理。