确保从关键部分之外以正确的顺序引发事件

时间:2015-11-03 08:37:23

标签: c#

Cosider以下示例类:

class MyClass
{
   private object syncRoot = new object();
   private int value;

   public event Action<int> SomethingOccurred;

   public void UpdateSomething()
   {
      int newValue;
      lock (syncRoot)
      {
          // ... Do some stuff that modifies some state of the object.
         newValue = ++value;
      }

      // How to ensure that raising these events are done in the correct order?
      SomethingOccurred?.Invoke(newValue);
   }
}

在上面的类中,事件可能不会以明显更新值的顺序发生,因为它是在lock-statement之外完成的。问题是,在lock语句的外部中引发这些事件的最佳方法是什么,但确保它们以正确的顺序引发(即在这种情况下产生序列1,2,3) ,4 ......)?

我提出的最好的想法是,实际上要添加ConcurrentQueue或类似的值,并且有一个单独的线程根据队列中的值引发事件。但我宁愿没有为提升这些事件而分配一个单独的线程。有没有更聪明的方法来实现这个目标?

编辑: 我的第一个想法是拥有并发队列,并使用以下代码来引发事件:

int result;
while (m_events.TryDequeue(out result))
   SomethingOccurred?.Invoke(result);

当然问题在于它也不能保证订单,因为多个线程会同时出现问题并且基本上存在同样的问题。

我可以在事件发生周围放置另一个锁,但是这会导致同样的意外阻塞,就像在第一时间从锁内部引发事件一样。

那么是否有一种无锁的方法来保证在这种情况下只有一个线程出列并引发事件?或者还有另一种更好的方式吗?

编辑2

为了说明用法,我想保证以下代码按顺序输出序列1到20:

MyClass myClass = new MyClass();

myClass.SomethingOccurred += (i) =>
{
   Thread.Sleep(100); Console.WriteLine(i);
};

Parallel.ForEach(Enumerable.Range(1, 20), i =>
        myClass.UpdateSomething());

我不关心是否从不同的线程调用事件处理程序,但不能同时调用它,并且必须以正确的顺序调用它。

到目前为止,我所拥有的最佳解决方案是以下可能不会非常有效地使用线程资源:

  class MyClass
  {
     private object syncRoot = new object();
     private int value;
     private readonly ConcurrentQueue<int> m_events = new ConcurrentQueue<int>();
     private object eventRaiserLock = new object();

     public event Action<int> SomethingOccurred;

     public void UpdateSomething()
     {
        int newValue;
        lock (syncRoot)
        {
           // ... Do some stuff that modifies some state of the object.
           newValue = ++value;
           m_events.Enqueue(newValue);
        }

        // How to ensure that raising these events are done in the correct order?
        RaiseEvents();
     }

     private void RaiseEvents()
     {
        Task.Run(() =>
        {
           lock (eventRaiserLock)
           {
              int result;
              while (m_events.TryDequeue(out result))
                 SomethingOccurred?.Invoke(result);
           }
        });
     }
  }    

1 个答案:

答案 0 :(得分:1)

如果您需要订购,则需要同步 - 就这么简单。

你在这里尝试做的事情并不完全明显 - 你所筹集的事件在一些随机线程中被有效提升。显然,这不会保留任何排序,因为事件可以同时运行(因为从多个线程调用UpdateSomething)。

队列是一个简单的解决方案,您也不需要浪费任何额外的线程 - 但是,您可能想要考虑UpdateSomething调用的顺序 - 您确定这些项目是否正在进行首先按照正确的顺序排队?

现在,ConcurrentQueue有点棘手,因为它没有给你一个漂亮的,等待的界面。一种选择是使用数据流库 - BufferBlock可以完成您想要的任务。否则,您可以编写自己的异步并发队列 - 尽管如此,这样做很复杂。你可以用这样的东西作为起点:

async Task Main()
{
  var queue = new AsyncConcurrentQueue<int>();
  var task = DequeueAllAsync(queue, i => Console.WriteLine(i));

  queue.Enqueue(1);
  queue.Enqueue(2);
  queue.Enqueue(3);
  queue.Enqueue(4);
  queue.Finish();

  await task;
}

private async Task DequeueAllAsync<T>(AsyncConcurrentQueue<T> queue, Action<T> action)
{
  try
  {
    while (true)
    {
      var value = await queue.TakeAsync(CancellationToken.None);

      action(value);
    }
  }
  catch (OperationCanceledException) { }
}

public class AsyncConcurrentQueue<T>
{
  private readonly ConcurrentQueue<T> _internalQueue;
  private readonly SemaphoreSlim _newItem;
  private int _isFinished;

  public AsyncConcurrentQueue()
  {
    _internalQueue = new ConcurrentQueue<T>();
    _newItem = new SemaphoreSlim(0);
  }

  public void Enqueue(T value)
  {
    _internalQueue.Enqueue(value);
    _newItem.Release();
  }

  public void Finish()
  {
    Interlocked.Exchange(ref _isFinished, 1);
    _newItem.Release();
  }

  public async Task<T> TakeAsync(CancellationToken token)
  {
    while (!token.IsCancellationRequested)
    {
      await _newItem.WaitAsync(token);

      token.ThrowIfCancellationRequested();

      T result;
      if (_internalQueue.TryDequeue(out result))
      {
        return result;
      }

      Interlocked.MemoryBarrier();

      if (_isFinished == 1) throw new OperationCanceledException();
    }

    throw new OperationCanceledException(token);
  }
}

这可确保您拥有一个具有全局排序的队列,您可以继续填充该队列,并且只要有任何项目,该队列就会不断清空。删除(和执行操作)是按添加顺序进行的,它发生在单个工作线程上。当没有要出队的项目时,该线程将返回到线程池,因此您不会浪费线程阻塞。

同样,这仍然是一个相对天真的解决方案。您希望至少添加更多错误处理(根据您的需要 - 例如,action(value)调用应该在try-catch中,以便失败的操作不会停止您的出队循环?)。