如何正确实现自定义等待者的OnCompleted方法? [英] How to implement the OnCompleted method of a custom awaiter correctly?

查看:83
本文介绍了如何正确实现自定义等待者的OnCompleted方法?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

这是 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 实现不太正确.

I suspect that my OnCompleted implementation is not quite correct.

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

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?

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

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.

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

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.

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

Update 2: Notable references I investigated:

服务员的工作方式:

一些实现:

  • 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

推荐答案

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

Schedules the continuation action that's invoked when the instance completes.

因此 OnCompleted 的两种实现都不是正确的",因为如果 awaitable 尚未完成,但请注册它以在 awaitable 完成时执行.

Hence neither of the implementations of the OnCompleted is "correct", because the awaiter shouldn't execute the passed delegate during that call if the awaitable is not already complete, but register it to be executed when the awaitable completes.

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

The only unclear is what the method should do if the awaitable is already complete at the time the method is called (although the compiler generated code does not call it in such case) - ignore the continuation delegate or execute. According to the Task implementation, it should be the later (execute).

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

Of course there are exceptions of the rule (hence the word "correct"). For instance, the YieldAwaiter specifically always returns IsCompleted == false to force calling it's OnCompleted method, which immediately schedules the passed delegate on the thread pool. But "normally" you won't do that.

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

Usually (as with the standard Task implementation) the awaitable will perform the operation, provide the result, the wait mechanism, and will maintain/execute the continuations. Their awaiters are usually structs holding the reference to the shared awaitable (along with continuation options when needed) and will delegate the GetResult and OnCompleted method calls to the shared awaitable, and specifically for OnCompleted passing the continuation delegate as well as options to the awaitable internal method responsible for registering/executing them. The "configurable" awaitables will simply hold the shared awaitable plus the options and simply pass them to the created awaiters.

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

Since in your example the waiting and result are provided by the awaiter, the awaitable can simply provide completion event:

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 完成事件,然后在该事件内部通过捕获的同步上下文或直接执行操作.

When OnCompleted is called, first we check if we are complete. If yes, we simply execute the passed delegate and return. Otherwise, we capture the synchronization context, subscribe on the awaitable completion event, and inside that event execute the action either via the captured synchronization context or directly.

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

Again, in the real life scenarios the awaitable should perform the real work, provide the result and maintain continuation actions, while awaiters should only register the continuation actions, eventually abstracting the continuation execution strategy - directly, via captured synchronization context, via thread pool etc.

这篇关于如何正确实现自定义等待者的OnCompleted方法?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆