如何减少连续触发事件的事件处理频率
How to reduce frequency of continuously fired event's event handling
我正在学习 c# 中的任务和 async/await
。所以请考虑我的问题的愚蠢。
class 中有一个事件 DummyEvent
。一个事件处理程序DummyEventHandler
订阅了这个event
,它处理大量的CPU绑定任务,实际上不需要那么频繁地使用。
因此,如果 DummyEvent
连续触发,我希望 DummyEventHandler
以降低的频率响应,或者在连续性结束时响应。
所以,我的想法是将大型任务提取到一个单独的任务中,并使其在继续之前延迟 500 毫秒。延迟结束后,它会检查是否再次安排了相同的任务(连续事件触发),如果为真,则避免大量计算。
这是我对这个想法的幼稚实现:
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 increase
if (ReducedCall == TotalCallActual)
{
// do all the large tasks
……
ReducedCall = 0;
TotalCallActual = 0;
}
}
但问题是,我没有得到我想要的。 Task.Delay(500)
行实际上并没有等待,或者,如果确实等待,则有问题,因为我遇到了惊人的问题。
任何更好的想法,或任何改进/更正?
询问任何其他信息。
谢谢
我会使用计时器事件处理程序而不是你的 DummyEventHandler
只需以毫秒为单位调整定时器的频率即可。您可以通过代码创建计时器,而无需将其作为控件添加到窗体中。我认为它在公共控件库中。
希望这对您有所帮助。祝你好运。
您可以利用 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
运算符将允许值(事件)每秒变为真。
所以在上面的代码示例中,文本 doing something 只会打印一次(一秒钟后),即使事件被触发多次。
编辑
顺便说一句,绿线的原因是你的任务没有等待。要修复它,请将代码更改为:
protected async void DummyEventHandler(object sender, bool arg)
{
TotalCallActual++;
await LargeCPUBoundTask(); // there is no more green underline here
}
不幸的是,这仍然不能解决您的问题,因为无法等待事件,因此如果在 LargeCPUBoundTask
仍然是 运行 时再次引发事件,将再次调用 LargeCPUBoundTask
所以如果你明白我的意思,工作就是重叠的。换句话说,这就是您的代码不起作用的原因。
我花了更多时间思考这个问题,我对第一个解决方案所做的假设是事件持续触发,而它可能只是在一段时间内触发部分时间,然后在 真正的问题。
在这种情况下,CPU 绑定任务只会在第一个事件触发时发生,如果事件在 CPU 绑定任务完成之前完成触发,则其余事件将不会获得处理。但是您不想处理所有这些,只处理 "last" 个(不一定是实际的最后一个,只需要处理 "cleanup" 个)。
所以我更新了我的答案以包括频繁但间歇性的用例(即事件爆发然后安静)会发生正确的事情并且 [=21= 的最终 运行 ] 绑定任务会发生(但一次仍然不超过 1 CPU 个绑定任务 运行ning)。
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();
}
}
我正在学习 c# 中的任务和 async/await
。所以请考虑我的问题的愚蠢。
class 中有一个事件 DummyEvent
。一个事件处理程序DummyEventHandler
订阅了这个event
,它处理大量的CPU绑定任务,实际上不需要那么频繁地使用。
因此,如果 DummyEvent
连续触发,我希望 DummyEventHandler
以降低的频率响应,或者在连续性结束时响应。
所以,我的想法是将大型任务提取到一个单独的任务中,并使其在继续之前延迟 500 毫秒。延迟结束后,它会检查是否再次安排了相同的任务(连续事件触发),如果为真,则避免大量计算。
这是我对这个想法的幼稚实现:
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 increase
if (ReducedCall == TotalCallActual)
{
// do all the large tasks
……
ReducedCall = 0;
TotalCallActual = 0;
}
}
但问题是,我没有得到我想要的。 Task.Delay(500)
行实际上并没有等待,或者,如果确实等待,则有问题,因为我遇到了惊人的问题。
任何更好的想法,或任何改进/更正?
询问任何其他信息。
谢谢
我会使用计时器事件处理程序而不是你的 DummyEventHandler 只需以毫秒为单位调整定时器的频率即可。您可以通过代码创建计时器,而无需将其作为控件添加到窗体中。我认为它在公共控件库中。
希望这对您有所帮助。祝你好运。
您可以利用 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
运算符将允许值(事件)每秒变为真。
所以在上面的代码示例中,文本 doing something 只会打印一次(一秒钟后),即使事件被触发多次。
编辑
顺便说一句,绿线的原因是你的任务没有等待。要修复它,请将代码更改为:
protected async void DummyEventHandler(object sender, bool arg)
{
TotalCallActual++;
await LargeCPUBoundTask(); // there is no more green underline here
}
不幸的是,这仍然不能解决您的问题,因为无法等待事件,因此如果在 LargeCPUBoundTask
仍然是 运行 时再次引发事件,将再次调用 LargeCPUBoundTask
所以如果你明白我的意思,工作就是重叠的。换句话说,这就是您的代码不起作用的原因。
我花了更多时间思考这个问题,我对第一个解决方案所做的假设是事件持续触发,而它可能只是在一段时间内触发部分时间,然后在 真正的问题。
在这种情况下,CPU 绑定任务只会在第一个事件触发时发生,如果事件在 CPU 绑定任务完成之前完成触发,则其余事件将不会获得处理。但是您不想处理所有这些,只处理 "last" 个(不一定是实际的最后一个,只需要处理 "cleanup" 个)。
所以我更新了我的答案以包括频繁但间歇性的用例(即事件爆发然后安静)会发生正确的事情并且 [=21= 的最终 运行 ] 绑定任务会发生(但一次仍然不超过 1 CPU 个绑定任务 运行ning)。
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();
}
}