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

时间:2014-04-04 02:07:24

标签: c# asynchronous async-await coroutine

我使用C#迭代器作为协同程序的替代品,它一直很好用。我想切换到async / await,因为我认为语法更清晰,它给了我类型安全。 In this (outdated) blog post, Jon Skeet shows a possible way to implement it

我选择采用略有不同的方式(通过实施我自己的SynchronizationContext并使用Task.Yield)。这很好。

然后我意识到会有问题;目前协程不必完成运行。它可以在任何产生的点上优雅地停止。我们可能有这样的代码:

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

private IEnumerator CoroutineMain()
{
    try
    {
        // Do something that runs over several frames
        yield return Coroutine.Sleep(5000);
    }
    finally
    {
        Log("Coroutine finished, either after 5 seconds, or because it was stopped");
    }
}

协程通过跟踪堆栈中的所有枚举器来工作。 C#编译器生成一个Dispose函数,可以调用该函数以确保在CoroutineMain中正确调用'finally'块,即使枚举未完成。通过这种方式,我们可以优雅地停止协程,并通过在堆栈上的所有Dispose对象上调用IEnumerator来确保调用finally块。这基本上是手动展开。

当我用async / await编写我的实现时,我意识到我们会失去这个功能,除非我弄错了。然后,我查找了其他协同解决方案,看起来Jon Skeet的版本看起来也不是以任何方式处理它。

我能想到的唯一方法就是拥有我们自己的自定义'Yield'函数,它会检查协程是否被停止,然后引发一个异常表明这一点。这将传播,执行finally块,然后被捕获到根附近的某处。我不觉得这很漂亮,因为第三方代码可能会捕获异常。

我是否误解了某些事情,这是否可以更容易地做到?或者我是否需要采用异常方式来执行此操作?

编辑:已经请求了更多信息/代码,所以这里有一些。我可以保证这只会在一个线程上运行,所以这里没有涉及线程。 我们当前的协程实现看起来有点像这样(这是简化的,但它适用于这个简单的情况):

public sealed class Coroutine : IDisposable
{
    private class RoutineState
    {
        public RoutineState(IEnumerator enumerator)
        {
            Enumerator = enumerator;
        }

        public IEnumerator Enumerator { get; private set; }
    }

    private readonly Stack<RoutineState> _enumStack = new Stack<RoutineState>();

    public Coroutine(IEnumerator enumerator)
    {
        _enumStack.Push(new RoutineState(enumerator));
    }

    public bool IsDisposed { get; private set; }

    public void Dispose()
    {
        if (IsDisposed)
            return;

        while (_enumStack.Count > 0)
        {
            DisposeEnumerator(_enumStack.Pop().Enumerator);
        }

        IsDisposed = true;
    }

    public bool Resume()
    {
        while (true)
        {
            RoutineState top = _enumStack.Peek();
            bool movedNext;

            try
            {
                movedNext = top.Enumerator.MoveNext();
            }
            catch (Exception ex)
            {
                // Handle exception thrown by coroutine
                throw;
            }

            if (!movedNext)
            {
                // We finished this (sub-)routine, so remove it from the stack
                _enumStack.Pop();

                // Clean up..
                DisposeEnumerator(top.Enumerator);


                if (_enumStack.Count <= 0)
                {
                    // This was the outer routine, so coroutine is finished.
                    return false;
                }

                // Go back and execute the parent.
                continue;
            }

            // We executed a step in this coroutine. Check if a subroutine is supposed to run..
            object value = top.Enumerator.Current;
            IEnumerator newEnum = value as IEnumerator;
            if (newEnum != null)
            {
                // Our current enumerator yielded a new enumerator, which is a subroutine.
                // Push our new subroutine and run the first iteration immediately
                RoutineState newState = new RoutineState(newEnum);
                _enumStack.Push(newState);

                continue;
            }

            // An actual result was yielded, so we've completed an iteration/step.
            return true;
        }
    }

    private static void DisposeEnumerator(IEnumerator enumerator)
    {
        IDisposable disposable = enumerator as IDisposable;
        if (disposable != null)
            disposable.Dispose();
    }
}

假设我们有以下代码:

private IEnumerator MoveToPlayer()
{
  try
  {
    while (!AtPlayer())
    {
      yield return Sleep(500); // Move towards player twice every second
      CalculatePosition();
    }
  }
  finally
  {
    Log("MoveTo Finally");
  }
}

private IEnumerator OrbLogic()
{
  try
  {
    yield return MoveToPlayer();
    yield return MakeExplosion();
  }
  finally
  {
    Log("OrbLogic Finally");
  }
}

这可以通过将OrbLogic枚举器的实例传递给Coroutine,然后运行它来创建。这允许我们每帧勾选协程。 如果玩家杀死了球,则协程未完成;只需在协程上调用Dispose即可。如果MoveTo逻辑上位于“try”块中,则在顶部IEnumerator调用Dispose会在语义上执行finally中的MoveTo块。然后,OrbLogic中的finally块将执行。 请注意,这是一个简单的案例,案例要复杂得多。

我正在努力在async / await版本中实现类似的行为。此版本的代码如下所示(省略错误检查):

public class Coroutine
{
    private readonly CoroutineSynchronizationContext _syncContext = new CoroutineSynchronizationContext();

    public Coroutine(Action action)
    {
        if (action == null)
            throw new ArgumentNullException("action");

        _syncContext.Next = new CoroutineSynchronizationContext.Continuation(state => action(), null);
    }

    public bool IsFinished { get { return !_syncContext.Next.HasValue; } }

    public void Tick()
    {
        if (IsFinished)
            throw new InvalidOperationException("Cannot resume Coroutine that has finished");

        SynchronizationContext curContext = SynchronizationContext.Current;
        try
        {
            SynchronizationContext.SetSynchronizationContext(_syncContext);

            // Next is guaranteed to have value because of the IsFinished check
            Debug.Assert(_syncContext.Next.HasValue);

            // Invoke next continuation
            var next = _syncContext.Next.Value;
            _syncContext.Next = null;

            next.Invoke();
        }
        finally
        {
            SynchronizationContext.SetSynchronizationContext(curContext);
        }
    }
}

public class CoroutineSynchronizationContext : SynchronizationContext
{
    internal struct Continuation
    {
        public Continuation(SendOrPostCallback callback, object state)
        {
            Callback = callback;
            State = state;
        }

        public SendOrPostCallback Callback;
        public object State;

        public void Invoke()
        {
            Callback(State);
        }
    }

    internal Continuation? Next { get; set; }

    public override void Post(SendOrPostCallback callback, object state)
    {
        if (callback == null)
            throw new ArgumentNullException("callback");

        if (Current != this)
            throw new InvalidOperationException("Cannot Post to CoroutineSynchronizationContext from different thread!");

        Next = new Continuation(callback, state);
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        throw new NotSupportedException();
    }

    public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
    {
        throw new NotSupportedException();
    }

    public override SynchronizationContext CreateCopy()
    {
        throw new NotSupportedException();
    }
}

我没有看到如何使用它来实现与迭代器版本类似的行为。 为漫长的代码提前道歉!

编辑2:新方法似乎有效。它允许我做类似的事情:

private static async Task Test()
{
    // Second resume
    await Sleep(1000);
    // Unknown how many resumes
}

private static async Task Main()
{
    // First resume
    await Coroutine.Yield();
    // Second resume
    await Test();
}

这为游戏构建AI提供了一种非常好的方式。

2 个答案:

答案 0 :(得分:11)

  

我使用C#迭代器作为协同程序的替代品,它一直都是   工作得很好。我想切换到async / await,因为我认为语法   更清洁,它给了我类型安全......

IMO,这是一个非常有趣的问题,虽然我需要一段时间才能完全理解它。也许,您没有提供足够的示例代码来说明这个概念。一个完整的应用程序会有所帮助,所以我会先尝试填补这个空白。以下代码说明了我理解的使用模式,如果我错了,请纠正我:

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

namespace ConsoleApplication
{
    // https://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();
        }
    }
}

此处,Resource.Dispose((IDisposable)enumerator).Dispose()而被调用。如果我们不打电话给enumerator.Dispose(),那么我们必须取消注释//while (e.MoveNext());并让迭代器优雅地完成,以便正确展开。

现在,我认为使用async/await实现此目标的最佳方法是使用custom awaiter

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

namespace ConsoleApplication
{
    // https://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();
                }
            }
        }
    }
}

当需要放松时,我在Awaiter.Dispose内请求取消并驱动状态机进入下一步(如果有待处理的继续)。这导致观察Awaiter.GetResult内的取消(由编译器生成的代码调用)。抛出TaskCanceledException并进一步展开using语句。因此,Resource得到妥善处理。最后,任务转换为已取消状态(task.IsCancelled == true)。

IMO,这是一种比在当前线程上安装自定义同步上下文更简单直接的方法。它可以很容易地适应多线程(更多细节here)。

这确实比IEnumerator / yield给你更多的自由。您可以在协同逻辑中使用try/catch,并且可以直接通过Task对象观察异常,取消和结果。

更新了,AFAIK在IDispose状态机方面,对于迭代器生成的async没有类比。当你想取消/解除它时,你真的必须驱动状态机。如果您想考虑使用try/catch来防止取消,我认为您可以做的最好的事情是检查_continuationAwaiter.Cancel是否为非空(在{{1}之后}并抛出致命异常out-of-the-band(使用帮助器MoveNext方法)。

答案 1 :(得分:7)

已更新,这已演变为博客文章: Asynchronous coroutines with C# 8.0 and IAsyncEnumerable


现在是2020年,我的await和协程的other answer在当今的C#语言标准中已经过时了。 C#8.0引入了对asynchronous streams的支持,并具有以下新功能:

为使自己熟悉异步流的概念,我强烈建议阅读Stephen Toub的"Iterating with Async Enumerables in C# 8"

这些新功能共同为以更自然的方式在C#中实现异步协同例程提供了良好的基础。

维基百科为what co-routines (aka corotines) generally are提供了很好的解释。我想在这里展示的是如何使用async来暂停协同例程await,并使用C#8.0任意地互换生产者/消费者的角色,从而暂停执行过程。

下面的代码片段应说明这一概念。在这里,我们有两个协程CoroutineACoroutineB,它们在伪线性执行流程继续进行时相互屈服,从而相互协作并异步执行。

namespace Tests
{
    [TestClass]
    public class CoroutineProxyTest
    {
        const string TRACE_CATEGORY = "coroutines";

        /// <summary>
        /// CoroutineA yields to CoroutineB
        /// </summary>
        private async IAsyncEnumerable<string> CoroutineA(
            ICoroutineProxy<string> coroutineProxy,
            [EnumeratorCancellation] CancellationToken token)
        {
            await using (var coroutine = await coroutineProxy.AsAsyncEnumerator(token))
            {
                const string name = "A";
                var i = 0;

                // yielding 1
                Trace.WriteLine($"{name} about to yeild: {++i}", TRACE_CATEGORY);
                yield return $"{i} from {name}";

                // receiving
                if (!await coroutine.MoveNextAsync())
                {
                    yield break;
                }
                Trace.WriteLine($"{name} received: {coroutine.Current}", TRACE_CATEGORY);

                // yielding 2
                Trace.WriteLine($"{name} about to yeild: {++i}", TRACE_CATEGORY);
                yield return $"{i} from {name}";

                // receiving
                if (!await coroutine.MoveNextAsync())
                {
                    yield break;
                }
                Trace.WriteLine($"{name} received: {coroutine.Current}", TRACE_CATEGORY);

                // yielding 3
                Trace.WriteLine($"{name} about to yeild: {++i}", TRACE_CATEGORY);
                yield return $"{i} from {name}";
            }
        }

        /// <summary>
        /// CoroutineB yields to CoroutineA
        /// </summary>
        private async IAsyncEnumerable<string> CoroutineB(
            ICoroutineProxy<string> coroutineProxy,
            [EnumeratorCancellation] CancellationToken token)
        {
            await using (var coroutine = await coroutineProxy.AsAsyncEnumerator(token))
            {
                const string name = "B";
                var i = 0;

                // receiving
                if (!await coroutine.MoveNextAsync())
                {
                    yield break;
                }
                Trace.WriteLine($"{name} received: {coroutine.Current}", TRACE_CATEGORY);

                // yielding 1
                Trace.WriteLine($"{name} about to yeild: {++i}", TRACE_CATEGORY);
                yield return $"{i} from {name}";

                // receiving
                if (!await coroutine.MoveNextAsync())
                {
                    yield break;
                }
                Trace.WriteLine($"{name} received: {coroutine.Current}", TRACE_CATEGORY);

                // yielding 2
                Trace.WriteLine($"{name} about to yeild: {++i}", TRACE_CATEGORY);
                yield return $"{i} from {name}";

                // receiving
                if (!await coroutine.MoveNextAsync())
                {
                    yield break;
                }
                Trace.WriteLine($"{name} received: {coroutine.Current}", TRACE_CATEGORY);
            }
        }

        /// <summary>
        /// Testing CoroutineA and CoroutineB cooperative execution
        /// </summary>
        [TestMethod] 
        public async Task Test_Coroutine_Execution_Flow()
        {
            // Here we execute two cotoutines, CoroutineA and CoroutineB,
            // which asynchronously yield to each other

            //TODO: test cancellation scenarios
            var token = CancellationToken.None;

            using (var apartment = new Tests.ThreadPoolApartment())
            {
                await apartment.Run(async () =>
                {
                    var proxyA = new CoroutineProxy<string>();
                    var proxyB = new CoroutineProxy<string>();

                    var listener = new Tests.CategoryTraceListener(TRACE_CATEGORY);
                    Trace.Listeners.Add(listener);
                    try
                    {
                        // start both coroutines
                        await Task.WhenAll(
                            proxyA.Run(token => CoroutineA(proxyB, token), token),
                            proxyB.Run(token => CoroutineB(proxyA, token), token))
                            .WithAggregatedExceptions();
                    }
                    finally
                    {
                        Trace.Listeners.Remove(listener);
                    }

                    var traces = listener.ToArray();
                    Assert.AreEqual(traces[0], "A about to yeild: 1");
                    Assert.AreEqual(traces[1], "B received: 1 from A");
                    Assert.AreEqual(traces[2], "B about to yeild: 1");
                    Assert.AreEqual(traces[3], "A received: 1 from B");
                    Assert.AreEqual(traces[4], "A about to yeild: 2");
                    Assert.AreEqual(traces[5], "B received: 2 from A");
                    Assert.AreEqual(traces[6], "B about to yeild: 2");
                    Assert.AreEqual(traces[7], "A received: 2 from B");
                    Assert.AreEqual(traces[8], "A about to yeild: 3");
                    Assert.AreEqual(traces[9], "B received: 3 from A");
                });
            }
        }
    }
}

测试的输出如下:

coroutines: A about to yeild: 1
coroutines: B received: 1 from A
coroutines: B about to yeild: 1
coroutines: A received: 1 from B
coroutines: A about to yeild: 2
coroutines: B received: 2 from A
coroutines: B about to yeild: 2
coroutines: A received: 2 from B
coroutines: A about to yeild: 3
coroutines: B received: 3 from A

我目前在某些自动化UI测试方案中使用异步协程。例如,我可能有一个异步测试工作流逻辑在UI线程(应该是CouroutineA上运行)和一个互补的工作流在ThreadPool线程上作为{{1}的一部分运行}方法(即[TestMethod])。

然后,我可以执行类似CouroutineB的操作,以在await WaitForUserInputAsync(); yield return true;CouroutineA合作执行流程的某些关键点进行同步。

没有CouroutineB,我将不得不使用某种形式的异步同步原语,例如Stephen Toub的AsyncManualResetEvent。我个人觉得使用协同例程是进行这种同步的一种更自然的方法。

yield return的代码(它驱动协同程序的执行)仍在开发中。目前,它使用TPL Dataflow的BufferBlock作为代理队列来协调异步执行,但我不确定这是否是一种最佳方式。当前,它是这样的:

CoroutineProxy