问题描述
我使用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).
这篇关于异步/等待作为替代品的协同程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!