我有一个数据源,每秒从15到20个线程生成〜1M亿个事件。
事件回调处理程序实现缓存策略,记录事件对象的更改(保证单个对象的更新始终来自同一个线程)
每100毫秒我想暂停/锁定事件处理程序并发布所有修改对象的最新状态的快照。
我目前看到的模拟实现:
private static void OnHandleManyEvents(FeedHandlerSource feedHandlerSource, MyObject myObject, ChangeFlags flags)
{
if (objectsWithChangeFlags[myObject.ID] == ChangeFlags.None)
{
UpdateStorage updateStorage = feedHandlerSourceToUpdateStorage[(int)feedHandlerSource];
lock (updateStorage.MyOjectUpdateLock)
{
objectsWithChangeFlags[myObject.ID] = objectsWithChangeFlags[myObject.ID] | flags;
updateStorage.MyUpdateObjects.Add(myObject);
}
} else
objectsWithChangeFlags[myObject.ID] = objectsWithChangeFlags[myObject.ID] | flags;
}
// runs on separate thread
private static void MyObjectPump()
{
while (true)
{
foreach (UpdateStorage updateStorage in feedHandlerSourceToUpdateStorage)
{
lock (updateStorage.MyOjectUpdateLock)
{
if (updateStorage.MyUpdateObjects.Count == 0)
continue;
foreach (MyObject myObject in updateStorage.MyUpdateObjects)
{
// do some stuff
objectsWithChangeFlags[myObject.ID] = ChangeFlags.None;
}
updateStorage.MyUpdateObjects.Clear();
}
}
Thread.Sleep(100);
}
}
此代码的问题虽然表现出良好的性能,但却是潜在的竞争条件。
具体来说,可能是对于Pump线程中的对象,ChangeFlags被设置为None,而事件回调将其设置回更改状态而不锁定资源(在这种情况下,对象永远不会被添加到MyObjectUpdates列表并永远保持陈旧状态。
另一种方法是锁定每个事件回调,这会导致太多的性能损失。
你会如何解决这个问题?
---更新--- 我相信我现在通过引入存储在objectsWithChangeFlags数组中的“CacheItem”来解决这个问题,该数组跟踪对象当前是否“已入队”。 我还测试了ConcurrentQueue的入队/出队,正如Holger在下面提到的那样,但它显示的吞吐量略低于仅使用锁(我猜是因为争用率不是很高而且没有争用的锁的开销非常低)
private class CacheItem
{
public ChangeFlags Flags;
public bool IsEnqueued;
}
private static void OnHandleManyEvents(MyObject myObject, ChangeFlags flags)
{
Interlocked.Increment(ref _countTotalEvents);
Interlocked.Increment(ref _countTotalEventsForInterval);
CacheItem f = objectsWithChangeFlags[myObject.Id];
if (!f.IsEnqueued)
{
Interlocked.Increment(ref _countEnqueue);
f.Flags = f.Flags | flags;
f.IsEnqueued = true;
lock (updateStorage.MyObjectUpdateLock)
updateStorage.MyObjectUpdates.Add(myObject);
}
else
{
Interlocked.Increment(ref _countCacheHits);
f.Flags = f.Flags | flags;
}
}
private static void QuotePump()
{
while (true)
{
lock (updateStorage.MyObjectUpdateLock)
{
foreach (var obj in updateStorage.MyObjectUpdates)
{
Interlocked.Increment(ref _countDequeue);
CacheItem f = objectsWithChangeFlags[obj.Id];
f.Flags = ChangeFlags.None;
f.IsEnqueued = false;
}
updateStorage.MyObjectUpdates.Clear();
}
_countQuotePumpRuns++;
Thread.Sleep(75);
}
}
答案 0 :(得分:1)
在类似的szenarios(日志记录线程)中,我使用了以下策略:
排队到ConcurrentQueue的事件。如果队列不为空,则Snapshot线程会暂时查看。如果没有,它会从中读取每个想法,直到它为空,执行更改然后拍摄快照。之后它可能会睡一会儿,或者立即再次检查是否有更多需要处理的事情,并且只是暂时不睡觉。
使用这种方法,您的事件将分批执行,并在每批次之后拍摄快照。
关于缓存:
我可以想象一个(并发)字典,您在其中查找事件处理程序中的对象。如果没有找到它,它的载荷(或者来自它)。事件处理后,它被添加(即使它已经在那里发现)。 Snapshot方法在快照它之前删除它从字典中快照的所有对象。然后,事件将在快照中,或者事件后对象仍将在Dictionary中。
这应该适用于您对一个对象的所有更改来自同一个线程的前提。字典将仅包含自上次快照运行以来更改的对象。
答案 1 :(得分:0)
你有两个objectsWithChangeFlags
个集合,每隔100毫秒切换一次参考吗?这样你就不必锁定任何东西,因为泵线程将在"离线"集合。