【问题标题】:How to make thread safe event handler如何制作线程安全的事件处理程序
【发布时间】:2016-05-02 09:39:10
【问题描述】:

在我的应用程序中,我有一个队列,它会在队列发生任何更改时触发通知,但有时会发生这种情况,当队列事件处理程序上有同时操作时,它会触发多次,这没关系,但我没有'不想要的是,...

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

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

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

【问题讨论】:

  • 有另一个布尔值告诉它当前是否正在执行?
  • 查看this支持页面。
  • @zee 尝试将 IsQueueInProcess 设为静态。
  • 你不能让NotificationQueue_Changed() 成为一个同步方法吗?这个await ProcessQueue() 没有多大意义,因为调用方法本身就是一个即发即弃的方法(使用无法等待的异步 void)。
  • IsQueueInProcess 只是静态的,但问题是有时会同时引发一些 NotificationQueue_Changed 事件,并且在 IsQueueInProcess 在 ProcessQeueue 中将更改设置为 true 之前,多个 ProcessQeueue 开始执行。我不想让 ProcessQeueue 同步,只想忽略正在运行的事件

标签: c# .net wpf multithreading


【解决方案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() 方法具有所需的同步,以保证如果任何线程已修改队列并导致引发此事件,该线程将不会失败启动另一轮处理应该第一次圆形无法观察到变化。

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

您的问题中没有足够的上下文,任何人都无法具体解决该问题。我只想指出,在尝试实现这种系统时,有人会忽略它是一件很常见的事情。恕我直言,更有理由只使用一个专用线程使用BlockingCollection<T> 来消耗添加到队列中的元素。 :)


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

【讨论】:

    【解决方案2】:

    我同意 Peter 的观点,即放弃基于事件的通知是最好的解决方案,并且您应该转移到生产者/消费者队列。但是,我推荐使用 TPL 数据流块之一,而不是 BlockingCollection<T>

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多