如何减少连续触发事件的事件处理频率

时间:2019-01-14 18:07:54

标签: c# async-await task-parallel-library

我正在学习c#中的任务和async/await。因此,请考虑我的问题的愚蠢性。

班上有一个事件DummyEvent。事件处理程序DummyEventHandler订阅了此event,它处理大量的CPU绑定任务,实际上并不需要如此频繁地使用它。

由于这个原因,如果DummyEvent连续被触发,我希望DummyEventHandler以降低的频率响应,或者在连续性结束时响应。

所以,我的想法是将大任务提取到单独的任务中,并使其延迟500毫秒,然后再继续执行。延迟结束后,它将检查是否再次计划了同一任务(连续事件触发),如果为true,则避免进行大量计算。

这是我对此想法的幼稚实现:

int ReducedCall = 0;
int TotalCallActual = 0;

protected void DummyEventHandler(object sender, bool arg)
{
    TotalCallActual++;
    LargeCPUBoundTask(); // there is a green underline here, but I think it's ok, or.. is it?
}

async Task LargeCPUBoundTask()
{
    ReducedCall = TotalCallActual;

    await Task.Delay(500);
    // if this task is called again in this time, TotalCallActual will inncrease

    if (ReducedCall == TotalCallActual)
    {
        // do all the large tasks
        ……

        ReducedCall = 0;
        TotalCallActual = 0;
    }
}

但是问题是,我没有得到想要的东西。 Task.Delay(500)行实际上并没有等待,或者,如果确实等待,则可能是因为我遇到了麻烦而出错。

有更好的主意,还是有任何改进/更正?

询问其他信息。

谢谢

3 个答案:

答案 0 :(得分:2)

您可以利用Reactive Extensions来做到这一点:

void Main()
{
    var generator = new EventGenerator();
    var observable = Observable.FromEventPattern<EventHandler<bool>, bool>(
                h => generator.MyEvent += h,
                h => generator.MyEvent -= h);

    observable
        .Throttle(TimeSpan.FromSeconds(1))
        .Subscribe(s =>
        {
            Console.WriteLine("doing something");
        });

    // simulate rapid firing event
    for(int i = 0; i <= 100; i++)
        generator.RaiseEvent(); 

    // when no longer interested, dispose the subscription  
    subscription.Dispose(); 
}

public class EventGenerator
{
    public event EventHandler<bool> MyEvent;

    public void RaiseEvent()
    {
        if (MyEvent != null)
        {
            MyEvent(this, false);
        }
    }
}

上面编码的Throttle运算符将使值(事件)每秒变为真。

因此在上面的代码示例中,即使事件被多次触发,文本做某事也只会打印一次(一秒钟后)。

修改
顺便说一句,绿线的原因是未等待您的任务。要对其进行修复,请将代码更改为:

protected async void DummyEventHandler(object sender, bool arg)
{
    TotalCallActual++;
    await LargeCPUBoundTask(); // there is no more green underline here
}

不幸的是,这仍然无法解决您的问题,因为无法等待某个事件,因此如果在LargeCPUBoundTask仍在运行时再次引发该事件,则会再次调用LargeCPUBoundTask,因此,如果你明白我的意思。换句话说,这就是为什么您的代码无法正常工作的原因。

答案 1 :(得分:1)

我将使用计时器事件处理程序代替您的DummyEventHandler 只要调整计时器的频率,就可以了。您可以通过代码创建计时器,而无需将其添加到窗体作为控件。我认为它在通用控件库中。

希望这会有所帮助。祝你好运。

答案 2 :(得分:1)

我花了更多时间考虑这个问题,我对第一个解决方案的假设是事件不断触发,而事件可能只是一段时间的一部分触发,然后停在真正的问题。

在这种情况下,CPU绑定任务将仅在第一个事件触发时发生,然后,如果事件在该CPU绑定任务完成之前触发,则其余事件将无法得到处理。但是,您并不想处理所有这些,仅处理“最后一个”(不一定是实际的最后一个,只需要再处理一个“清理”)即可。

因此,我更新了答案,以包含经常但断断续续的事件(即突发事件然后安静)的用例,这将发生正确的事情,并且将发生CPU绑定任务的最终运行(但仍然没有更多)一次运行1个CPU绑定任务)。

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        Sender s = new Sender();
        using (Listener l = new Listener(s))
        {
            s.BeginDemonstration();
        }
    }
}

class Sender
{
    const int ATTEMPTED_CALLS = 1000000;

    internal EventHandler frequencyReducedHandler;
    internal int actualCalls = 0;
    internal int ignoredCalls = 0;

    Task[] tasks = new Task[ATTEMPTED_CALLS];

    internal void BeginDemonstration()
    {
        int attemptedCalls;
        for (attemptedCalls = 0; attemptedCalls < ATTEMPTED_CALLS; attemptedCalls++)
        {
            tasks[attemptedCalls] = Task.Run(() => frequencyReducedHandler.Invoke(this, EventArgs.Empty));
            //frequencyReducedHandler?.BeginInvoke(this, EventArgs.Empty, null, null);
        }
        if (tasks[0] != null)
        {
            Task.WaitAll(tasks, Timeout.Infinite);
        }
        Console.WriteLine($"Attempted: {attemptedCalls}\tActual: {actualCalls}\tIgnored: {ignoredCalls}");
        Console.ReadKey();
    }
}

class Listener : IDisposable
{
    enum State
    {
        Waiting,
        Running,
        Queued
    }

    private readonly AutoResetEvent m_SingleEntry = new AutoResetEvent(true);
    private readonly Sender m_Sender;

    private int m_CurrentState = (int)State.Waiting;

    internal Listener(Sender sender)
    {
        m_Sender = sender;
        m_Sender.frequencyReducedHandler += Handler;
    }

    private async void Handler(object sender, EventArgs args)
    {
        int state = Interlocked.Increment(ref m_CurrentState);
        try
        {
            if (state <= (int)State.Queued) // Previous state was WAITING or RUNNING
            {
                // Ensure only one run at a time
                m_SingleEntry.WaitOne();
                try
                {
                    // Only one thread at a time here so
                    // no need for Interlocked.Increment
                    m_Sender.actualCalls++;
                    // Execute CPU intensive task
                    await Task.Delay(500);
                }
                finally
                {
                    // Allow a waiting thread to proceed
                    m_SingleEntry.Set();
                }
            }
            else
            {
                Interlocked.Increment(ref m_Sender.ignoredCalls);
            }
        }
        finally
        {
            Interlocked.Decrement(ref m_CurrentState);
        }
    }

    public void Dispose()
    {
        m_SingleEntry?.Dispose();
    }
}