建议数据结构/同步方法

时间:2015-03-17 15:28:00

标签: c# multithreading race-condition

我有一个数据源,每秒从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);
        }
    }

2 个答案:

答案 0 :(得分:1)

在类似的szenarios(日志记录线程)中,我使用了以下策略:

排队到ConcurrentQueue的事件。如果队列不为空,则Snapshot线程会暂时查看。如果没有,它会从中读取每个想法,直到它为空,执行更改然后拍摄快照。之后它可能会睡一会儿,或者立即再次检查是否有更多需要处理的事情,并且只是暂时不睡觉。

使用这种方法,您的事件将分批执行,并在每批次之后拍摄快照。

关于缓存:

我可以想象一个(并发)字典,您在其中查找事件处理程序中的对象。如果没有找到它,它的载荷(或者来自它)。事件处理后,它被添加(即使它已经在那里发现)。 Snapshot方法在快照它之前删除它从字典中快照的所有对象。然后,事件将在快照中,或者事件后对象仍将在Dictionary中。

这应该适用于您对一个对象的所有更改来自同一个线程的前提。字典将仅包含自上次快照运行以来更改的对象。

答案 1 :(得分:0)

你有两个objectsWithChangeFlags个集合,每隔100毫秒切换一次参考吗?这样你就不必锁定任何东西,因为泵线程将在"离线"集合。