在筹集活动时我应该如何实施“安静时期”?

时间:2010-08-24 14:19:25

标签: c# events

我正在使用订阅者/通知程序模式来提升和使用C#中的.Net中间层事件。例如,当从导入文件的批处理程序中持久保存数据时,某些事件会以“突发”形式引发。这执行一个可能长时间运行的任务,我希望通过实现“静默期”来避免每秒多次触发事件,从而事件系统等待直到事件流减慢以处理事件。

如果发布者在通知订阅者方面发挥积极作用,我该如何做?我不想等到事件进来检查是否还有其他人等待安静的时期......

目前没有主机进程轮询订阅模型。我应该放弃发布/订阅模式还是有更好的方法?

3 个答案:

答案 0 :(得分:1)

我不确定我是否正确理解了您的问题,但我会尝试在源头解决问题 - 确保事件不会在“爆发”中引发。您可以考虑实施批处理操作,可以从文件导入程序中使用。这将被视为你的中间人的单一事件,并举起一个单一事件。

如果您无法进行上述更改,我认为实施一些合理的解决方案将非常棘手 - 您可以尝试将您的发布者包装在“缓存”发布者中,该发布者会实现一些启发式来缓存事件,如果他们正在爆发。如果当前正在处理另一个相同类型的事件,最简单的方法是缓存一个事件(因此,您的批处理将导致至少2个事件 - 一个在最开始,一个在最后)。您可以等待很短的时间,并且只在下一个事件未在此期间发生时引发事件,但即使管道中存在单个事件,您也会遇到时滞。您还需要确保即使有不断的事件排队,您也会不时地举起活动 - 否则出版商可能会变得饥饿。

第二个选项很难实现,并且会包含启发式算法,这可能会非常错误......

答案 1 :(得分:1)

这是一个粗略的实现,可能会指向你的方向。在我的示例中,涉及通知的任务是保存数据对象。保存对象时,将引发Saved事件。除了简单的Save方法之外,我还实现了BeginSave和EndSave方法以及Save的重载,它可以与这两个方法一起用于批量保存。调用EndSave时,会触发一个BatchSaved事件。

显然,您可以根据自己的需要进行更改。在我的示例中,我跟踪了批处理操作期间保存的所有对象的列表,但这可能不是您需要做的事情...您可能只关心保存了多少对象甚至只是批量保存操作已完成。如果您预计会保存大量对象,那么将它们存储在列表中(如我的示例中)可能会成为内存问题。

编辑:我在我的示例中添加了一个“阈值”概念,试图阻止大量对象被保存在内存中。但是,这会导致BatchSaved事件更频繁地触发。我还添加了一些锁定来解决潜在的线程安全问题,尽管我可能已经错过了一些东西。

class DataConcierge<T>
{
    // *************************
    // Simple save functionality
    // *************************

    public void Save(T dataObject)
    {
        // perform save logic

        this.OnSaved(dataObject);
    }

    public event DataObjectSaved<T> Saved;

    protected void OnSaved(T dataObject)
    {
        var saved = this.Saved;
        if (saved != null)
            saved(this, new DataObjectEventArgs<T>(dataObject));
    }

    // ************************
    // Batch save functionality
    // ************************

    Dictionary<BatchToken, List<T>> _BatchSavedDataObjects = new Dictionary<BatchToken, List<T>>();
    System.Threading.ReaderWriterLockSlim _BatchSavedDataObjectsLock = new System.Threading.ReaderWriterLockSlim();

    int _SavedObjectThreshold = 17; // if the number of objects being stored for a batch reaches this threshold, then those objects are to be cleared from the list.

    public BatchToken BeginSave()
    {
        // create a batch token to represent this batch
        BatchToken token = new BatchToken();

        _BatchSavedDataObjectsLock.EnterWriteLock();
        try
        {
            _BatchSavedDataObjects.Add(token, new List<T>());
        }
        finally
        {
            _BatchSavedDataObjectsLock.ExitWriteLock();
        }
        return token;
    }

    public void EndSave(BatchToken token)
    {
        List<T> batchSavedDataObjects;
        _BatchSavedDataObjectsLock.EnterWriteLock();
        try
        {
            if (!_BatchSavedDataObjects.TryGetValue(token, out batchSavedDataObjects))
                throw new ArgumentException("The BatchToken is expired or invalid.", "token");

            this.OnBatchSaved(batchSavedDataObjects); // this causes a single BatchSaved event to be fired

            if (!_BatchSavedDataObjects.Remove(token))
                throw new ArgumentException("The BatchToken is expired or invalid.", "token");
        }
        finally
        {
            _BatchSavedDataObjectsLock.ExitWriteLock();
        }
    }

    public void Save(BatchToken token, T dataObject)
    {
        List<T> batchSavedDataObjects;
        // the read lock prevents EndSave from executing before this Save method has a chance to finish executing
        _BatchSavedDataObjectsLock.EnterReadLock();
        try
        {
            if (!_BatchSavedDataObjects.TryGetValue(token, out batchSavedDataObjects))
                throw new ArgumentException("The BatchToken is expired or invalid.", "token");

            // perform save logic

            this.OnBatchSaved(batchSavedDataObjects, dataObject);
        }
        finally
        {
            _BatchSavedDataObjectsLock.ExitReadLock();
        }
    }

    public event BatchDataObjectSaved<T> BatchSaved;

    protected void OnBatchSaved(List<T> batchSavedDataObjects)
    {
        lock (batchSavedDataObjects)
        {
            var batchSaved = this.BatchSaved;
            if (batchSaved != null)
                batchSaved(this, new BatchDataObjectEventArgs<T>(batchSavedDataObjects));
        }
    }

    protected void OnBatchSaved(List<T> batchSavedDataObjects, T savedDataObject)
    {
        // add the data object to the list storing the data objects that have been saved for this batch
        lock (batchSavedDataObjects)
        {
            batchSavedDataObjects.Add(savedDataObject);

            // if the threshold has been reached
            if (_SavedObjectThreshold > 0 && batchSavedDataObjects.Count >= _SavedObjectThreshold)
            {
                // then raise the BatchSaved event with the data objects that we currently have
                var batchSaved = this.BatchSaved;
                if (batchSaved != null)
                    batchSaved(this, new BatchDataObjectEventArgs<T>(batchSavedDataObjects.ToArray()));

                // and clear the list to ensure that we are not holding on to the data objects unnecessarily
                batchSavedDataObjects.Clear();
            }
        }
    }
}

class BatchToken
{
    static int _LastId = 0;
    static object _IdLock = new object();

    static int GetNextId()
    {
        lock (_IdLock)
        {
            return ++_LastId;
        }
    }

    public BatchToken()
    {
        this.Id = GetNextId();
    }

    public int Id { get; private set; }
}

class DataObjectEventArgs<T> : EventArgs
{
    public T DataObject { get; private set; }

    public DataObjectEventArgs(T dataObject)
    {
        this.DataObject = dataObject;
    }
}

delegate void DataObjectSaved<T>(object sender, DataObjectEventArgs<T> e);

class BatchDataObjectEventArgs<T> : EventArgs
{
    public IEnumerable<T> DataObjects { get; private set; }

    public BatchDataObjectEventArgs(IEnumerable<T> dataObjects)
    {
        this.DataObjects = dataObjects;
    }
}

delegate void BatchDataObjectSaved<T>(object sender, BatchDataObjectEventArgs<T> e);

在我的示例中,我选择使用令牌概念来创建单独的批处理。这允许在不同线程上运行的较小批处理操作完成并引发事件,而无需等待更大的批处理操作完成。

我制作了separete事件:Saved和BatchSaved。但是,这些可以很容易地合并为一个事件。

编辑:Steven Sudit在访问活动代表时指出的固定竞争条件。

编辑:在我的示例中修改了锁定代码以使用ReaderWriterLockSlim而不是Monitor(即“lock”语句)。我认为存在一些竞争条件,例如Save和EndSave方法之间。 EndSave可能会执行,导致数据对象列表从字典中删除。如果Save方法在另一个线程上同时执行,则可以将数据对象添加到该列表中,即使它已经从字典中删除了。

在我修改过的示例中,这种情况不会发生,如果在EndSave之后执行,Save方法将抛出异常。这些竞争条件主要是由于我试图避免我认为不必要的锁定。我意识到需要更多代码才能锁定,但决定使用ReaderWriterLockSlim而不是Monitor,因为我只想阻止Save和EndSave同时执行;没有必要阻止多个线程同时执行Save。请注意,Monitor仍然用于同步对从字典中检索的特定数据对象列表的访问。

编辑:添加了用法示例

以下是上述示例代码的使用示例。

    static void DataConcierge_Saved(object sender, DataObjectEventArgs<Program.Customer> e)
    {
        Console.WriteLine("DataConcierge<Customer>.Saved");
    }

    static void DataConcierge_BatchSaved(object sender, BatchDataObjectEventArgs<Program.Customer> e)
    {
        Console.WriteLine("DataConcierge<Customer>.BatchSaved: {0}", e.DataObjects.Count());
    }

    static void Main(string[] args)
    {
        DataConcierge<Customer> dc = new DataConcierge<Customer>();
        dc.Saved += new DataObjectSaved<Customer>(DataConcierge_Saved);
        dc.BatchSaved += new BatchDataObjectSaved<Customer>(DataConcierge_BatchSaved);

        var token = dc.BeginSave();
        try
        {
            for (int i = 0; i < 100; i++)
            {
                var c = new Customer();
                // ...
                dc.Save(token, c);
            }
        }
        finally
        {
            dc.EndSave(token);
        }
    }

这导致以下输出:

  

DataConcierge&lt; Customer&gt; .BatchSaved:17

     

DataConcierge&lt; Customer&gt; .BatchSaved:17

     

DataConcierge&lt; Customer&gt; .BatchSaved:17

     

DataConcierge&lt; Customer&gt; .BatchSaved:17

     

DataConcierge&lt; Customer&gt; .BatchSaved:17

     

DataConcierge&lt; Customer&gt; .BatchSaved:15

我的示例中的阈值设置为17,因此一批100个项目会导致BatchSaved事件触发6次。

答案 2 :(得分:0)

这是一个刚刚脱离我脑海中的想法。我不知道它是多么可行,也看不出一种明显的方式使它更通用,但它可能是一个开始。它所做的就是为按钮点击事件提供一个缓冲区(必要时替换你的事件)。

class ButtonClickBuffer
{
    public event EventHandler BufferedClick;

    public ButtonClickBuffer(Button button, int queueSize)
    {
        this.queueSize= queueSize;
        button.Click += this.button_Click;
    }

    private int queueSize;
    private List<EventArgs> queuedEvents = new List<EventArgs>();

    private void button_Click(object sender, EventArgs e)
    {
        queuedEvents.Add(e);
        if (queuedEvents.Count >= queueSize)
        {
            if (this.BufferedClick!= null)
            {
                foreach (var args in this.queuedEvents)
                {
                    this.BufferedClick(sender, args);
                }
                queuedEvents.Clear();
            }
        }
    }
}

所以您的订阅者,而不是订阅:

this.button1.Click += this.button1_Click;

将使用缓冲区,指定要等待的事件数量:

ButtonClickBuffer buffer = new ButtonClickBuffer(this.button1, 5);
buffer.BufferedClick += this.button1_Click;

它以一种简单的测试形式工作,但我还没有准备就绪!

你说你不想等待一个事件来查看队列是否在等待,这正是这样做的。您可以替换缓冲区内的逻辑来生成一个新线程,该线程监视队列并根据需要调度事件。上帝知道可能会出现什么线程和锁定问题!