如何制作线程安全事件处理程序

时间:2016-05-01 07:48:54

标签: c# .net wpf multithreading

在我的应用程序中,我有一个队列,只要队列发生任何更改就会触发通知,但有时会发生当队列事件处理程序上有多个同时操作时它会多次触发并且没有问题,但我不想要的是,......

以下是事件处理程序的代码:

private async void NotificationQueue_Changed(object sender, EventArgs e)
{
  if (!IsQueueInProcess)
    await ProcessQeueue();
}

ProcessQueue方法中,我将IsQueueInProcess设置为true,无论何时完成,都将其设置为false。现在,问题是每当多个事件通知同时触发多个ProcessQeueue方法开始执行时,我不想要。我想确保在任何给定时间只执行一次ProcessQeueue

2 个答案:

答案 0 :(得分:1)

鉴于您的声明,只要队列发生任何更改就会引发此事件,并且队列可以同时使用(即有多个生产者向队列中添加内容),我觉得最好的方法是解决这个问题就是完全放弃基于事件的行为。相反,使用BlockingCollection<T>,使用专用于通过GetConsumingEnumerable()处理队列的线程。只要队列为空,该方法就会阻塞线程,并且只要任何其他线程向其添加内容,线程就会允许线程删除和处理队列中的项。集合本身是线程安全的,所以使用它你不需要任何额外的线程同步(对于队列的处理,即......处理项目可能涉及线程交互,但是你的问题没有描述这个方面,所以我不能说出一种方式或其他任何事情。)

也就是说,从字面上看问题,最简单的方法是包含一个信号量:

private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);

private async void NotificationQueue_Changed(object sender, EventArgs e)
{
    if (_semaphore.Wait(0))
    {
        await ProcessQueue();
        _semaphore.Release();
    }
}

以上尝试获取信号量的锁定。如果超时为0毫秒,即使无法获取信号量,它也会立即返回。返回值表示信号量是否已成功获取。

这样,只要没有未完成的队列处理操作,当前事件处理程序调用就可以获取信号量并调用ProcessQueue()方法。该操作完成后,继续将释放信号量。在此之前,事件处理程序的其他任何调用都不能获取信号量,因此不会启动队列处理。


我会注意到,这里没有任何东西可以保证线程相互竞争的解决方案可以确保队列始终为空,或者始终对其进行一些处理操作。这取决于您,以确保ProcessQueue()方法具有所需的同步以保证如果任何线程已修改队列并导致引发此事件,则该线程将不会无法启动另一轮处理第一轮无法观察到变化。

或者换句话说,您需要确保对于将要引发该事件的任何线程,当前处理操作将观察到对队列的更改,或者该线程将启动一个新线程。 / p>

在你的问题中没有足够的背景可供任何人专门解决这个问题。我只想指出,在尝试实现这种系统时,有人忽略了这一点。恕我直言,更有理由只使用BlockingCollection<T>使用专用线程来消耗添加到队列中的元素。 :)


另请参阅相关问题How to avoid reentrancy with async void event handlers?。这是一个稍微不同的问题,因为接受的答案会导致事件处理程序的每次调用都导致事件处理程序启动的操作。您的场景更简单,因为您只是想跳过新操作的启动,但您仍然可以在那里找到一些有用的见解。

答案 1 :(得分:1)

我同意Peter的意见,放弃基于事件的通知是最佳解决方案,您应该转移到生产者/消费者队列。但是,我建议使用其中一个TPL数据流块而不是BlockingCollection<T>

特别是ActionBlock<T>应该可以很好地运作:

private readonly ActionBlock<T> notificationQueue = new ActionBlock<T>(async t =>
{
  await ProcessQueueItem(t);
});

默认情况下,TPL Dataflow块的并发限制为1.