等待作为替代品的协同程序

等待作为替代品的协同程序

本文介绍了异步/等待作为替代品的协同程序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我使用C#迭代器作为协同程序的替代品,它已被伟大的工作。我想切换到异步/等待,因为我认为语法是清洁的,这让我类型安全。
在此(过时)的博客文章,乔恩斯基特显示来实现它一个可行的办法。

我选择去一个稍微不同的方式(通过实现我自己的的SynchronizationContext ,并使用 Task.Yield )。这工作得很好。

然后我意识到会有问题;目前,协程没有完成运行。它可以摆好在那里它产生的任何点被停止。我们可能有code是这样的:

 私人IEnumerator的睡眠(INT毫秒)
{
    秒表计时= Stopwatch.StartNew();
    做
    {
        产量返回NULL;
    }
    而(timer.ElapsedMilliseconds<毫秒);
}私人IEnumerator的CoroutineMain()
{
    尝试
    {
        //请在需要运行在几个帧的东西
        产量返回Coroutine.Sleep(5000);
    }
    最后
    {
        日志(协程完成后,无论是在5秒钟后,或者因为它被停止);
    }
}

该协程的工作原理是跟踪所有统计员在堆栈中。 C#编译器生成一个的Dispose 函数可以调用,以确保'终于'块在 CoroutineMain 正确地调用即使枚举没有完成。这样我们就可以正常停止协程,终于还是确保块调用,调用的Dispose 上的所有的IEnumerator 对象在堆栈中。这基本上是手动平仓。

当我写我的实现与异步/等待我意识到,我们将失去这个功能,除非我错了。然后我抬头等协程解决方案,它看起来并不像乔恩斯基特的版本以任何方式处理它的。

我能想到的来处理,这将是有我们自己的自定义收益功能,这将检查协程被停止,然后提高了表明这一例外的唯一方式。这会向上传递,最后执行的块,然后在某处根附近抓获。我不觉得,虽然这pretty,作为第三方code可能捕获该异常。

我误解的东西,而这是不可能在一个更简单的方法呢?或者,我需要去除外办法做到这一点?

编辑:更多信息/ code已被要求,所以这里的一些。我可以保证这将是对只有一个线程中运行,所以没有这里涉及线程。
我们目前的协同程序的实现看起来有点像这样(这是简化的,但它在这个简单的情况下有效):

 公共密封类协程:IDisposable接口
{
    私有类RoutineState
    {
        公共RoutineState(IEnumerator的枚举)
        {
            枚举=枚举;
        }        公众的IEnumerator枚举{搞定;私人集; }
    }    私人只读堆栈< RoutineState> _enumStack =新的堆栈< RoutineState>();    市民协程(IEnumerator的枚举)
    {
        _enumStack.Push(新RoutineState(枚举));
    }    公共BOOL IsDisposed {搞定;私人集; }    公共无效的Dispose()
    {
        如果(IsDisposed)
            返回;        而(_enumStack.Count大于0)
        {
            DisposeEnumerator(_enumStack.Pop()枚举。);
        }        IsDisposed = TRUE;
    }    公共BOOL简历()
    {
        而(真)
        {
            RoutineState顶= _enumStack.Peek();
            布尔movedNext;            尝试
            {
                movedNext = top.Enumerator.MoveNext();
            }
            赶上(异常前)
            {
                //处理异常被抛出协程
                扔;
            }            如果(!movedNext)
            {
                //我们完成了这个(子)例程,所以从栈中删除
                _enumStack.Pop();                // 清理..
                DisposeEnumerator(top.Enumerator);
                如果(_enumStack.Count&下; = 0)
                {
                    //这是外例程,所以协程结束。
                    返回false;
                }                //返回并执行父。
                继续;
            }            //我们在执行本协程的一个步骤。检查是否有子程序应该运行..
            对象值= top.Enumerator.Current;
            IEnumerator的newEnum =值的IEnumerator;
            如果(newEnum!= NULL)
            {
                //我们目前的枚举,得到一个新的枚举,这是一个子程序。
                //推动我们新的子程序,并立即执行第一次迭代
                RoutineState newState =新RoutineState(newEnum);
                _enumStack.Push(newState);                继续;
            }            //实际的结果是产生了,所以我们已经完成了一个迭代/步。
            返回true;
        }
    }    私有静态无效DisposeEnumerator(IEnumerator的枚举)
    {
        IDisposable的一次性=枚举为IDisposable的;
        如果(一次性!= NULL)
            disposable.Dispose();
    }
}

假设我们有code这样的:

 私人的IEnumerator MoveToPlayer()
{
  尝试
  {
    而(!AtPlayer())
    {
      产量返回睡眠(500); //对玩家移动两次每秒
      CalculatePosition();
    }
  }
  最后
  {
    日志(的moveTo最后);
  }
}私人IEnumerator的OrbLogic()
{
  尝试
  {
    产量返回MoveToPlayer();
    产量返回MakeExplosion();
  }
  最后
  {
    日志(OrbLogic最后);
  }
}

这将通过传递OrbLogic枚举的一个实例协同程序,然后运行它来创建。这使我们能够勾选协程每一帧。 如果玩家杀死宝珠,协程没有完成运行;处置只是呼吁协同程序。如果通过MoveTo 在'尝试'块在逻辑上,然后在上面调用Dispose 的IEnumerator 将语义,使通过MoveTo 最后块执行。然后事后在OrbLogic终于块将被执行。
注意,这是一个简单的例子和​​情况要复杂得多。

我在努力实现异步/的await版本类似的行为。在code这个版本看起来像这样(检查省略错误):

 公共类协程
{
    私人只读CoroutineSynchronizationContext _syncContext =新CoroutineSynchronizationContext();    市民协程(动作动作)
    {
        如果(动作== NULL)
            抛出新的ArgumentNullException(行动);        _syncContext.Next =新CoroutineSynchronizationContext.Continuation(州=>动作(),NULL);
    }    公共BOOL IsFinished {{返回_syncContext.Next.HasValue!; }}    公共无效蜱()
    {
        如果(IsFinished)
            抛出新的InvalidOperationException异常(无法恢复协程已完成了);        SynchronizationContext的curContext = SynchronizationContext.Current;
        尝试
        {
            SynchronizationContext.SetSynchronizationContext(_syncContext);            //接下来是保证有因为IsFinished检查的价值
            Debug.Assert的(_syncContext.Next.HasValue);            //调用一个延续
            VAR下一= _syncContext.Next.Value;
            _syncContext.Next = NULL;            next.Invoke();
        }
        最后
        {
            SynchronizationContext.SetSynchronizationContext(curContext);
        }
    }
}公共类CoroutineSynchronizationContext:的SynchronizationContext
{
    内部结构延续
    {
        公共延续(SendOrPostCallback回调,对象的状态)
        {
            回调=回;
            状态=状态;
        }        公共SendOrPostCallback回调;
        公共对象国;        公共无效的invoke()
        {
            回调(州);
        }
    }    内部延续?接下来{搞定;组; }    公共覆盖无效后(SendOrPostCallback回调,对象的状态)
    {
        如果(回调== NULL)
            抛出新的ArgumentNullException(回调);        如果(当前!=这一点)
            抛出新的InvalidOperationException异常(无法从不同的线程发布到CoroutineSynchronizationContext!);        下一步=新的延续(回调状态);
    }    公共覆盖无效发送(SendOrPostCallback D,对象状态)
    {
        抛出新NotSupportedException异常();
    }    公共覆盖INT等待(IntPtr的[] waitHandles,布尔为WaitAll,INT millisecondsTimeout)
    {
        抛出新NotSupportedException异常();
    }    公众覆盖SynchronizationContext的CreateCopy()
    {
        抛出新NotSupportedException异常();
    }
}

我不知道如何使用这个实施类似行为的迭代器版本。
提前道歉冗长code!

编辑2:这种新方法似乎是工作。它可以让我做的东西,如:

 私有静态异步任务测试()
{
    //其次简历
    等待睡眠(1000);
    //未知有多少简历
}私有静态异步任务的Main()
{
    //首先简历
    等待Coroutine.Yield();
    //其次简历
    等待测试();
}

其中规定建立AI游戏的一个非常好的途径。


解决方案

IMO, it's a very interesting question, although it took me awhile to fully understand it. Perhaps, you didn't provide enough sample code to illustrate the concept. A complete app would help, so I'll try to fill this gap first. The following code illustrates the usage pattern as I understood it, please correct me if I'm wrong:

using System;
using System.Collections;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    // http://stackoverflow.com/q/22852251/1768303

    public class Program
    {
        class Resource : IDisposable
        {
            public void Dispose()
            {
                Console.WriteLine("Resource.Dispose");
            }

            ~Resource()
            {
                Console.WriteLine("~Resource");
            }
        }

        private IEnumerator Sleep(int milliseconds)
        {
            using (var resource = new Resource())
            {
                Stopwatch timer = Stopwatch.StartNew();
                do
                {
                    yield return null;
                }
                while (timer.ElapsedMilliseconds < milliseconds);
            }
        }

        void EnumeratorTest()
        {
            var enumerator = Sleep(100);
            enumerator.MoveNext();
            Thread.Sleep(500);
            //while (e.MoveNext());
            ((IDisposable)enumerator).Dispose();
        }

        public static void Main(string[] args)
        {
            new Program().EnumeratorTest();
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
            GC.WaitForPendingFinalizers();
            Console.ReadLine();
        }
    }
}

Here, Resource.Dispose gets called because of ((IDisposable)enumerator).Dispose(). If we don't call enumerator.Dispose(), then we'll have to uncomment //while (e.MoveNext()); and let the iterator finish gracefully, for proper unwinding.

Now, I think the best way to implement this with async/await is to use a custom awaiter:

using System;
using System.Collections;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    // http://stackoverflow.com/q/22852251/1768303
    public class Program
    {
        class Resource : IDisposable
        {
            public void Dispose()
            {
                Console.WriteLine("Resource.Dispose");
            }

            ~Resource()
            {
                Console.WriteLine("~Resource");
            }
        }

        async Task SleepAsync(int milliseconds, Awaiter awaiter)
        {
            using (var resource = new Resource())
            {
                Stopwatch timer = Stopwatch.StartNew();
                do
                {
                    await awaiter;
                }
                while (timer.ElapsedMilliseconds < milliseconds);
            }
            Console.WriteLine("Exit SleepAsync");
        }

        void AwaiterTest()
        {
            var awaiter = new Awaiter();
            var task = SleepAsync(100, awaiter);
            awaiter.MoveNext();
            Thread.Sleep(500);

            //while (awaiter.MoveNext()) ;
            awaiter.Dispose();
            task.Dispose();
        }

        public static void Main(string[] args)
        {
            new Program().AwaiterTest();
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
            GC.WaitForPendingFinalizers();
            Console.ReadLine();
        }

        // custom awaiter
        public class Awaiter :
            System.Runtime.CompilerServices.INotifyCompletion,
            IDisposable
        {
            Action _continuation;
            readonly CancellationTokenSource _cts = new CancellationTokenSource();

            public Awaiter()
            {
                Console.WriteLine("Awaiter()");
            }

            ~Awaiter()
            {
                Console.WriteLine("~Awaiter()");
            }

            public void Cancel()
            {
                _cts.Cancel();
            }

            // let the client observe cancellation
            public CancellationToken Token { get { return _cts.Token; } }

            // resume after await, called upon external event
            public bool MoveNext()
            {
                if (_continuation == null)
                    return false;

                var continuation = _continuation;
                _continuation = null;
                continuation();
                return _continuation != null;
            }

            // custom Awaiter methods
            public Awaiter GetAwaiter()
            {
                return this;
            }

            public bool IsCompleted
            {
                get { return false; }
            }

            public void GetResult()
            {
                this.Token.ThrowIfCancellationRequested();
            }

            // INotifyCompletion
            public void OnCompleted(Action continuation)
            {
                _continuation = continuation;
            }

            // IDispose
            public void Dispose()
            {
                Console.WriteLine("Awaiter.Dispose()");
                if (_continuation != null)
                {
                    Cancel();
                    MoveNext();
                }
            }
        }
    }
}

When it's time to unwind, I request the cancellation inside Awaiter.Dispose and drive the state machine to the next step (if there's a pending continuation). This leads to observing the cancellation inside Awaiter.GetResult (which is called by the compiler-generated code). That throws TaskCanceledException and further unwinds the using statement. So, the Resource gets properly disposed of. Finally, the task transitions to the cancelled state (task.IsCancelled == true).

IMO, this is a more simple and direct approach than installing a custom synchronization context on the current thread. It can be easily adapted for multithreading (some more details here).

This should indeed give you more freedom than with IEnumerator/yield. You could use try/catch inside your coroutine logic, and you can observe exceptions, cancellation and the result directly via the Task object.

Updated, AFAIK there is no analogy for the iterator's generated IDispose, when it comes to async state machine. You really have to drive the state machine to an end when you want to cancel/unwind it. If you want to account for some negligent use of try/catch preventing the cancellation, I think the best you could do is to check if _continuation is non-null inside Awaiter.Cancel (after MoveNext) and throw a fatal exception out-of-the-band (using a helper async void method).

这篇关于异步/等待作为替代品的协同程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

09-05 11:21