简单的内存中消息队列

时间:2016-04-05 20:07:11

标签: c# queue domain-driven-design semaphore reentrancy

我们现有的域事件实现限制(通过阻止)一次发布到一个线程,以避免对处理程序的重新调用:

public interface IDomainEvent {}  // Marker interface

public class Dispatcher : IDisposable
{
    private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);

    // Subscribe code...

    public void Publish(IDomainEvent domainEvent)
    {
        semaphore.Wait();
        try
        {
            // Get event subscriber(s) from concurrent dictionary...

            foreach (Action<IDomainEvent> subscriber in eventSubscribers)
            {
                subscriber(domainEvent);
            }
        }
        finally
        {
            semaphore.Release();
        }
    }
    // Dispose pattern...
}

如果处理程序发布事件,则会发生死锁。

如何重写此序列化对Publish的调用?换句话说,如果订阅处理程序A发布事件B,我将获得:

  1. 处理程序A名为
  2. 处理程序B呼叫
  3. 同时在多线程环境中保留对处理程序无可重入调用的条件。

    我不想更改公共方法签名;例如,应用程序中没有地方可以调用方法来发布队列。

3 个答案:

答案 0 :(得分:1)

使用该界面无法完成。您可以异步处理事件订阅以在仍然以串行方式运行它们时删除死锁,但是您无法保证所描述的顺序。对事件A的处理程序正在运行但在它发布事件B之前,对Publish的另一个调用可能会排队某事(事件C)。然后事件B在队列中的事件C之后结束。

只要Handler A在获取队列中的项目时与其他客户端处于同等地位,它就必须像其他人一样等待(死锁)或者必须公平地玩(先到先得) 。你在那里的界面不允许对两者进行不同的处理。

这并不是说你无法在你的逻辑中找到一些恶作剧来尝试区分它们(例如基于线程id或其他可识别的东西),但如果你不控制它们,那么沿着这些线路的任何东西都是不可靠的用户代码。

答案 1 :(得分:1)

您必须使Publish异步才能实现。天真的实施将如下:

public class Dispatcher : IDisposable {
    private readonly BlockingCollection<IDomainEvent> _queue = new BlockingCollection<IDomainEvent>(new ConcurrentQueue<IDomainEvent>());
    private readonly CancellationTokenSource _cts = new CancellationTokenSource();

    public Dispatcher() {
        new Thread(Consume) {
            IsBackground = true
        }.Start();
    }

    private List<Action<IDomainEvent>> _subscribers = new List<Action<IDomainEvent>>();

    public void AddSubscriber(Action<IDomainEvent> sub) {
        _subscribers.Add(sub);
    }

    private void Consume() {            
        try {
            foreach (var @event in _queue.GetConsumingEnumerable(_cts.Token)) {
                try {
                    foreach (Action<IDomainEvent> subscriber in _subscribers) {
                        subscriber(@event);
                    }
                }
                catch (Exception ex) {
                    // log, handle                        
                }
            }
        }
        catch (OperationCanceledException) {
            // expected
        }
    }

    public void Publish(IDomainEvent domainEvent) {
        _queue.Add(domainEvent);
    }

    public void Dispose() {
        _cts.Cancel();
    }
}

答案 2 :(得分:1)

我们提出了一种同步实现的方法。

public class Dispatcher : IDisposable
{
    private readonly ConcurrentQueue<IDomainEvent> queue = new ConcurrentQueue<IDomainEvent>();
    private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);

    // Subscribe code...

    public void Publish(IDomainEvent domainEvent)
    {
        queue.Enqueue(domainEvent);

        if (IsPublishing)
        {
            return;
        }

        PublishQueue();
    }

    private void PublishQueue()
    {
        IDomainEvent domainEvent;
        while (queue.TryDequeue(out domainEvent))
        {
            InternalPublish(domainEvent);
        }
    }

    private void InternalPublish(IDomainEvent domainEvent)
    {
        semaphore.Wait();
        try
        {
            // Get event subscriber(s) from concurrent dictionary...

            foreach (Action<IDomainEvent> subscriber in eventSubscribers)
            {
                subscriber(domainEvent);
            }
        }
        finally
        {
            semaphore.Release();
        }

        // Necessary, as calls to Publish during publishing could have queued events and returned.
        PublishQueue();
    }

    private bool IsPublishing
    {
        get { return semaphore.CurrentCount < 1; }
    }
    // Dispose pattern for semaphore...
}

}