代码之家  ›  专栏  ›  技术社区  ›  György Kőszeg

如何正确实现自定义等待器的oncompleted方法?

  •  2
  • György Kőszeg  · 技术社区  · 7 年前

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

    我试图挖掘 ConfiguredTaskAwaiter 由返回 Task.ConfigureAwait(bool).GetAwaiter() 方法,可以看到黑色魔法发生在 SynchronizationContextAwaitTaskContinuation 类,但这是一个内部类型,以及许多其他内部使用的类型。有没有方法可以重构我的 完成 按预期实施?

    更新 : 投反对票者须知 我知道我在 完成 这就是我问的原因。如果您对质量(或其他任何问题)有任何疑问,请发表评论并帮助我改进问题,这样我也可以帮助您更好地突出问题。谢谢。

    注释2 :我知道我可以使用 TaskCompletionSource<TResult> 它的规律 Task<TResult> 结果,但我想了解背景。 这是唯一的动机。纯粹的好奇心。

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

    侍者如何工作:

    一些实现:

    3 回复  |  直到 7 年前
        1
  •  4
  •   Ivan Stoev    7 年前

    的msdn解释 OnCompleted 方法是:

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

    因此 完成 是“正确的”,因为 awaiter 如果 awaitable 尚未完成,但在 等待 完成。

    唯一不清楚的是,如果 等待 在调用方法时已经完成(尽管编译器生成的代码在这种情况下不调用它)-忽略继续委托或执行。根据 Task 实现,应该是以后的(执行)。

    当然,规则也有例外(因此 “正确” )例如, YieldAwaiter 特别是总是返回 IsCompleted == false 强迫打电话 完成 方法,该方法立即在线程池上调度传递的委托。但“通常”你不会这么做。

    通常(如标准 任务 实施) 等待 将执行操作、提供结果、等待机制,并将维护/执行延续。他们 awaiters 通常是 struct s保存对共享的引用 等待 (以及必要时的延续选项)并将 GetResult 完成 对共享的方法调用 等待 特别是为了 完成 将延续委托和选项传递给 等待 负责注册/执行它们的内部方法。“可配置” 等待 S将简单地持有共享的 等待 加上选项,然后简单地将它们传递给 等待者 S.

    因为在您的示例中,等待和结果由 等待者 , the 等待 只需提供完成事件:

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

    以及 等待者 S将订阅:

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

    什么时候? 完成 是的,首先我们检查一下是否完成。如果是,我们只需执行传递的委托并返回。否则,我们捕获同步上下文,订阅 等待 完成事件,在该事件内部,通过捕获的同步上下文或直接执行操作。

    同样,在现实生活中, 等待 应执行实际工作,提供结果并保持持续行动,同时 等待者 S应该只注册延续动作,最终通过捕获的同步上下文、线程池等直接抽象延续执行策略。

        2
  •  0
  •   György Kőszeg    7 年前

    注: 最初,在@ivanstoev给出正确答案后,我把这个答案作为总结放在问题的末尾(非常感谢启蒙运动)。现在我把这部分提取成一个真正的答案。


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

    1。测试应用程序

    WinForms应用程序(也可以是其他单线程用户界面)具有 ProgressBar 和3 Button 控件:一个按钮只启动一个异步操作(和一个进度条),其他按钮在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。海关等候班

    与问题中的原始示例不同,这里的可等待类本身包含延续,在执行完成后调用延续。因此,等待者只需请求安排后续执行的继续。

    请注意 配置等待 GetAwaiter 基本相同-后者可以使用默认配置。

    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 .

    其次,请注意,现在等待者也有一个 等待者 只返回自身的方法。这是为了 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();
            });
        }
    }
    
        3
  •  -1
  •   Paulo Morgado    7 年前

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

    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 了解它是如何工作的。