避免使用单独的线程从BlockingCollection获取重复值

时间:2013-03-18 20:18:12

标签: c# .net multithreading timer duplicates

我从历史数据库接收大约50 Hz的数据。我想在10 Hz左右将其传输到另一端。 为了达到这个目的,我产生了两个定时器,一个用于从历史数据库中获取数据(我的运行速度是发送定时器的两倍)。第二个定时器,为200ms(10 Hz)。 第一个计时器存储它在BlockingCollection中获得的每个值(我尝试过ConcurrentQueue)。由于两个不同的线程读/写集合,我不能使用普通的列表/队列。 这样做的挑战是它不会给我一个甚至50Hz。我希望每次通话都能获得5个左右的值,但它会以爆发形式提供数据(有时甚至没有,有时为15)。因此,我将BlockingCollection作为缓冲区,发送计时器从中获取数据。 发送计时器使用递归方法获取列表中间的数据。它检查值是否已发送,如果已经是(50Hz - > 10Hz),则获取下一个第5个值,如果不发送该值。 现在到了问题;递归方法有时会发送两次相同的值,即使我锁定了对象。我们已经确定问题是锁定没有按预期工作,但我们不知道为什么或如何解决它。

有什么建议吗? 附加的代码不是真正的代码,但它说明了问题,并且实际上为您提供了重复值,尽管不是很常见。

class Program {

    private readonly ConcurrentDictionary<int, BlockingCollection<TestObject>> _pipes = new ConcurrentDictionary<int, BlockingCollection<TestObject>>();
    private const int Interval = 5;

    private Timer inTimer;
    private Timer outTimer;

    static void Main() {
        Program program = new Program();
        program.Run();
        Console.ReadLine();
    }

    private void Run() {
        _pipes[100] = new BlockingCollection<TestObject>();
        _pipes[200] = new BlockingCollection<TestObject>();
        _pipes[300] = new BlockingCollection<TestObject>();

        inTimer = new Timer(InTimer, null, 0, 100);
        Thread.Sleep(1000);
        outTimer = new Timer(OutTimer, null, 0, 200);
    }

    private void OutTimer(object state) {
        foreach (TestObject testObject in GetNotSentTestObjects()) {
            Console.WriteLine("{0};{1};{2}", testObject.PipeId, testObject.Timestamp.ToString("o"), testObject.Value);
        }
    }

    private IEnumerable<TestObject> GetNotSentTestObjects() {
        List<TestObject> testObjects = new List<TestObject>();

        foreach (KeyValuePair<int, BlockingCollection<TestObject>> pipe in _pipes) {
            TestObject testObject = GetLatestTestObjectNotSent(pipe.Key, (pipe.Value.Count / 2));
            if (testObject == null) {
                return null;
            }
            testObjects.Add(testObject);
        }
        return testObjects;
    }

    private TestObject GetLatestTestObjectNotSent(int key, int locationInList) {

        BlockingCollection<TestObject> pipe = _pipes[key];
        TestObject testObject;
        lock (pipe) {
            testObject = pipe.ElementAt(locationInList - 1);

            if (testObject.Sent) {
                int nextLocationInList = locationInList + Interval;
                GetLatestTestObjectNotSent(key, nextLocationInList);
            }
            testObject.Sent = true;
        }
        testObject.PipeId = key;
        return testObject;
    }

    private void InTimer(object sender) {
        Random random = new Random();
        for (int i = 0; i < random.Next(0,20); i++) {
            foreach (KeyValuePair<int, BlockingCollection<TestObject>> pipe in _pipes) {
                pipe.Value.Add(new TestObject());
            }
            i++;
        }
    }
}

internal class TestObject {
    public DateTime Timestamp { get; set; }
    public string Value { get; set; }
    public bool Sent { get; set; }
    public int PipeId;

    public TestObject() {
        Value = Guid.NewGuid().ToString().Substring(0, 8);
        Timestamp = DateTime.Now;
    }
}

注意,Thread.Sleep是要填充列表。在我的代码中,我还有一些代码可以在一段时间后修剪列表。

3 个答案:

答案 0 :(得分:1)

BlockingCollection是一个队列。它不是为随机访问而设计的,并且试图随机访问它会导致你没有遇到麻烦。你也没有从队列中删除项目,这意味着你最终会耗尽内存。

您可能遇到的一个问题是,虽然您在使用ElementAt访问列表时锁定了列表,但在添加项目时却没有锁定它。但是锁定并发数据结构的整个想法应该让你重新考虑你的设计。您不应该必须锁定并发数据结构。

据我了解,您想要选择尚未处理的每第5个项目。如果您不想丢弃未处理的项目(即项目1-4),那将成为一个相当困难的问题。如果您要丢弃这些项目,那么调用Take五次就是一件简单的事情:

TestObject o;
for (int i = 0; i < 5; ++i)
{
    o = pipe.Take();
}
// You now have the 5th item from the queue
// The other items have been discarded

如果您想保留这些项目以便稍后进行处理,则必须以某种方式将它们添加回队列。但它们会到达队列的末尾,这意味着您将在较新的项目之前处理旧项目。

此外,在某些时候你会填满队列,因为生产者加的东西比他们消费的速度快。

您需要更多地考虑您希望此应用程序如何工作,或者更好地向我们解释它。你所描述的内容以及你发布的代码都很混乱,使我们无法提出好的建议。

答案 1 :(得分:0)

相同的线程可以多次获得相同的锁。见这个例子

object o = new object();
lock (o) lock (o) Console.WriteLine("Acquired lock two times");

您应该使用其他同步原语,如Mutex,Semaphore,AutoResetEvent等...

答案 2 :(得分:0)

经过大量的抨击后,当我使用递归方法时,我需要返回...