是否有“单项大小的异步任务缓冲区”这样的同步工具?

很多时候在UI开发中我处理事件的方式是当事件第一次出现时 – 我立即开始处理,但如果有一个处理操作正在进行中 – 我等待它完成,然后再处理另一个事件。 如果在操作完成之前发生多个事件 – 我只处理最近的事件。

我通常这样做的方式我的进程方法有一个循环,在我的事件处理程序中,我检查一个字段,指示我当前是否正在处理某些事情,如果我 – 我把我当前的事件参数放在另一个基本上是一个项目大小的字段中缓冲区和当前处理过程完成时 – 我检查是否有其他事件要处理,我循环直到我完成。

现在这似乎有点过于重复,可能不是最优雅的方式,虽然它似乎对我来说工作正常。 那我有两个问题:

  1. 我需要做的是否有名字?
  2. 是否有一些可重复使用的同步类型可以为我做到这一点?

我正在考虑在我的工具包中添加一些由Stephen Toub提供的异步协调原语。

首先,我们将处理您描述的情况,其中始终使用该方法来自UI线程或其他一些同步上下文。 Run方法本身可以async处理通过同步上下文的所有编组。

如果我们正在运行,我们只需设置下一个存储的操作。 如果我们不是,那么我们就表明我们现在正在运行,等待行动,然后继续等待下一步行动,直到没有下一步行动。 我们确保无论何时完成,我们都会表明我们已经完成了运行:

 public class EventThrottler { private Func next = null; private bool isRunning = false; public async void Run(Func action) { if (isRunning) next = action; else { isRunning = true; try { await action(); while (next != null) { var nextCopy = next; next = null; await nextCopy(); } } finally { isRunning = false; } } } private static Lazy defaultInstance = new Lazy(() => new EventThrottler()); public static EventThrottler Default { get { return defaultInstance.Value; } } } 

因为至少一般来说,这个类只能从UI线程中使用,所以通常只需要一个,所以我添加了一个默认实例的便利属性,但是因为它可能仍然有意义比起一个节目中的一个,我没有把它变成单身。

Run接受一个Func ,认为它通常是一个异步lambda。 它可能看起来像:

 public class Foo { public void SomeEventHandler(object sender, EventArgs args) { EventThrottler.Default.Run(async () => { await Task.Delay(1000); //do other stuff }); } } 

好的,所以,只是为了冗长,这里有一个版本来处理从不同线程调用事件处理程序的情况。 我知道你说你假设他们都是从UI线程中调用的,但我对它进行了一些概括。 这意味着锁定对lock块中类型的实例字段的所有访问,但不实际执行lock块内的函数。 最后一部分不仅对性能很重要,要确保我们不会阻止项目只是设置next字段,而且还要避免该操作也会调用run的问题,这样它就不需要处理重入问题或潜在的僵局。 这种模式,在锁定块中执行操作然后根据锁定中确定的条件进行响应意味着设置局部变量以指示在锁定结束后应该执行的操作。

 public class EventThrottlerMultiThreaded { private object key = new object(); private Func next = null; private bool isRunning = false; public void Run(Func action) { bool shouldStartRunning = false; lock (key) { if (isRunning) next = action; else { isRunning = true; shouldStartRunning = true; } } Action continuation = null; continuation = task => { Func nextCopy = null; lock (key) { if (next != null) { nextCopy = next; next = null; } else { isRunning = false; } } if (nextCopy != null) nextCopy().ContinueWith(continuation); }; if (shouldStartRunning) action().ContinueWith(continuation); } } 

我需要做的是否有名字?

您所描述的内容听起来有点像蹦床和崩溃队列。 蹦床基本上是一个迭代调用thunk返回函数的循环。 一个例子是Reactive Extensions中的CurrentThreadScheduler 。 在CurrentThreadScheduler上调度项目时,工作项将添加到调度程序的线程本地队列中,之后将发生以下事件之一:

  1. 如果trampoline已在运行(即当前线程已经在处理线程本地队列),则Schedule()调用立即返回。
  2. 如果trampoline 没有运行(即,没有工作项在当前线程上排队/运行),则当前线程开始处理线程本地队列中的项目,直到它为空,此时调用Schedule()回报。

折叠队列累积要处理的项目,并添加扭曲,如果队列中已有等效项目,则该项目将简单地替换为较新项目(导致只有最新的等效项目保留在队列中,与两者相反)。 我们的想法是避免处理过时/过时的事件。 考虑市场数据的消费者(例如,股票价格)。 如果您收到经常交易的安全性的多个更新,则每次更新都会使早期更新过时。 如果已经到达更近期的滴答,则可能没有必要处理相同证券的早期滴答。 因此,折叠队列是合适的。

在您的场景中,您基本上有一个处理折叠队列的蹦床,所有传入事件都被认为是等效的。 这导致有效的最大队列大小为1,因为添加到非空队列的每个项目都将导致现有项目被逐出。

是否有一些可重复使用的同步类型可以为我做到这一点?

我不知道现有的解决方案能满足您的需求,但您当然可以创建一个能够支持可插拔调度策略的通用蹦床或事件循环。 默认策略可以使用标准队列,而其他策略可以使用优先级队列或折叠队列。

您所描述的内容与TPL Dataflow的BrodcastBlock行为非常相似:它始终只记住您发送给它的最后一项。 如果将它与执行操作的ActionBlock结合使用,并且仅具有当前正在处理的项目的容量,则可以获得所需的内容(该方法需要更好的名称):

 // returns send delegate private static Action CreateProcessor(Action executedAction) { var broadcastBlock = new BroadcastBlock(null); var actionBlock = new ActionBlock( executedAction, new ExecutionDataflowBlockOptions { BoundedCapacity = 1 }); broadcastBlock.LinkTo(actionBlock); return item => broadcastBlock.Post(item); } 

用法可能是这样的:

 var processor = CreateProcessor( i => { Console.WriteLine(i); Thread.Sleep(i); }); processor(100); processor(1); processor(2); 

输出:

 100 2