这里有一个粗略的实现,可能指向你的方向。在我的示例中,涉及通知的任务是保存数据对象。当一个对象被保存时,会引发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而不是监视器(即“锁”语句)。我认为存在一些竞争条件,例如在Save和EndSave方法之间。 EndSave可能执行,导致数据对象列表从字典中删除。如果Save方法在另一个线程上同时执行,则可能会将数据对象添加到该列表中,即使它已从字典中删除。
在我的修改示例中,这种情况不会发生,并且Save方法在EndSave之后执行时会引发异常。这些竞争条件主要是由我试图避免我认为是不必要的锁定造成的。我意识到更多的代码需要在锁内,但决定使用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 <顾客> .BatchSaved:17
DataConcierge <顾客> .BatchSaved:17
DataConcierge <顾客> .BatchSaved :17
Dat aConcierge <顾客> .BatchSaved:17
DataConcierge <顾客> .BatchSaved:17
DataConcierge <顾客> .BatchSaved:15
在我的示例中的阈值设置为17,所以一批100个项目导致BatchSaved事件触发6次。
标准投诉:不要检查委托为空,然后调用它,因为这表现出竞争条件。相反,做一个本地副本,检查它为空,然后通过它调用。 – 2010-08-24 18:06:52
啊,是的。感谢您指出了这一点。我已根据您的建议进行了更新。 – 2010-08-24 18:42:17