【问题标题】:How to implement the OnCompleted method of a custom awaiter correctly?如何正确实现自定义等待者的 OnCompleted 方法?
【发布时间】:2018-07-17 07:14:40
【问题描述】:

我有一个自定义的可等待类型,问题是继续在不同的线程上恢复,这会导致 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 实现不太正确。

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

更新反对者注意:我知道我在OnCompleted 做了不正当的事情,这就是我问的原因。如果您对质量(或其他任何事情)有疑问,请发表评论并帮助我改进问题,以便我也可以帮助您更好地突出问题。谢谢。

注意 2:我知道我可以使用TaskCompletionSource<TResult> 及其常规Task<TResult> 结果的解决方法,但我想了解背景。 这是唯一的动力。纯粹的好奇心。

更新 2:我调查的重要参考资料:

等待者的工作原理:

一些实现:

【问题讨论】:

  • 您需要在运行 awaitable 之前捕获同步上下文。
  • 即使我在构造函数中捕获它,结果也是一样的。
  • 为什么要使用 Task 构造函数,为什么要在其中使用这些参数?
  • @PauloMorgado:continuation 是我要执行的委托。 LongRunning 选项是对TaskScheduler 的提示,表示执行可以持续很长时间。默认调度程序实现为此类任务创建一个新的Thread,而不是使用ThreadPool(但实际上可以忽略)。为什么 ctor:我猜你在问为什么不 Task.RunTask.Factory.StartNew。这是因为我不需要等到Task 被安排好并且它的状态在启动后变为正在运行。
  • @taffer 好的。我会删除我之前的评论,以免引起负面评论。

标签: c# async-await


【解决方案1】:

OnCompleted方法的MSDN解释是:

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

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

唯一不清楚的是,如果在调用方法时awaitable 已经完成(尽管在这种情况下编译器生成的代码不会调用它),该方法应该做什么——忽略延续委托或执行。按照Task的实现,应该是后者(执行)。

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

通常(与标准的Task 实现一样)awaitable 将执行操作、提供结果、等待机制,并将维护/执行延续。他们的awaiters 通常是structs 持有对共享awaitable 的引用(以及需要时的延续选项),并将GetResultOnCompleted 方法调用委托给共享awaitable,专门用于OnCompleted 将延续委托以及选项传递给负责注册/执行它们的awaitable 内部方法。 “可配置”awaitables 将简单地保存共享的awaitable 以及选项,并将它们简单地传递给创建的awaiters。

由于在您的示例中等待和结果由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);
}

awaiters 会订阅它:

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 应该执行实际工作,提供结果并维护延续动作,而 awaiters 应该只注册延续动作,最终抽象延续执行策略 - 直接,通过通过线程池等捕获同步上下文。

【讨论】:

  • 哇,这真的很有启发性。这是我在任何地方都找不到的缺失信息。 MSDN 对此守口如瓶,没有一个示例,并且大量其他参考资料从未涵盖此场景(至少我找不到任何东西)。承诺就是承诺,有你的赏金(我暂时不允许颁奖)。在那之前,把我的赞成票和确认作为接受的答案。
  • 完全同意——MSDN 应该很好地解释实现者的期望,以及每个播放器的目的、方法和调用时间,而不是从链接中简单总结。看起来他们只为他们的实现保留“高级”的东西:)
【解决方案2】:

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

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 以了解其工作原理。

【讨论】:

  • 是的,我怀疑有类似的事情,但我仍然不知道我应该如何做到这一点。现在我正在使用TaskCompletionSource 来实现相同的目标,但如果我有时间,也许我会再做一次。谢谢你的链接——我经常阅读 Jon Skeet 的材料,所以我什至不明白我是怎么找不到这个的。一旦我有足够的时间,我会经历它。
  • 这是解决op提供的代码中实际问题的答案
【解决方案3】:

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


因此,根据 Ivan 的回答,这里有一个包含缺失部分的小摘要,我认为应该在文档中。下面的示例也模仿了TaskConfigureAwait 行为。

1.测试应用

带有 ProgressBar 和 3 个 Button 控件的 WinForms 应用程序(也可以是其他单线程 UI):一个按钮简单地启动异步操作(和进度条),其他按钮在UI 线程或在外部线程中。

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

2。自定义等待类

与问题中的原始示例相反,这里的可等待类本身包含延续,它在执行完成后被调用。因此,awaiter 可以请求安排后续执行以供以后执行。

并且请注意ConfigureAwaitGetAwaiter基本相同——后者可以使用默认配置。

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

3.等待者

OnCompleted 现在不执行延续(与我调查的示例不同),而是通过调用 MyAwaitable.ScheduleContinuation 来注册它以供以后使用。

其次,请注意,现在等待者也有一个 GetAwaiter 方法,它只是返回自己。这是await myAwaitable.ConfigureAwait(bool) 使用所必需的。

public class MyAwaiter : INotifyCompletion
{
    private readonly MyAwaitable awaitable;
    private readonly bool captureContext;

    public MyAwaiter(MyAwaitable awaitable, bool captureContext)
    {
        this.awaitable = awaitable;
        this.captureContext = captureContext;
    }

    public MyAwaiter GetAwaiter() => this;

    public bool IsCompleted => awaitable.IsCompleted;

    public int GetResult() => awaitable.RunToCompletionAndGetResult();

    public void OnCompleted(Action continuation)
    {
        var capturedContext = SynchronizationContext.Current;
        awaitable.ScheduleContinuation(() =>
        {
            if (captureContext && capturedContext != null)
                capturedContext.Post(_ => continuation(), null);
            else
                continuation();
        });
    }
}

【讨论】:

    猜你喜欢
    • 2018-02-13
    • 2016-01-10
    • 2017-09-09
    • 2018-07-26
    • 2013-07-21
    • 1970-01-01
    • 2015-10-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多