队列.同步化是否比使用锁更快?

8
我有一个队列,其中Enqueue操作由一个线程执行,而Dequeue操作由另一个线程执行。不用说,我必须为它实现一些线程安全措施。
我首先尝试在每个Enqueue/Dequeue之前对队列使用锁定,因为它可以更好地控制锁定机制。它运行良好,但是我的好奇心驱使我进行了更多测试。
然后,我尝试使用Queue.Synchronized包装器,保持其他所有内容不变。现在,我不确定是否正确,但是这种方法似乎比第一种方法略微快一点。
你认为这两种方法之间实际上存在一些性能差异,还是我只是在想象? :)

3
使用任一方法取出锁非常快。性能下降源于争夺这些锁的程度。 - serg10
3个回答

18
当请求 Queue.Synchronized 时,会返回一个 SynchronizedQueue ,它在对内部队列的 EnqueueDequeue 调用时仅最小程度地使用了 lock。因此,性能应该与手动使用 lockEnqueueDequeue 进行管理的使用 Queue 相同。
你确实在想象事情 - 它们应该是相同的。 更新 实际上,当使用 SynchronizedQueue 时,您添加了一个间接的层,因为您必须通过包装器方法来访问其正在管理的内部队列。如果有什么不同,这应该非常微小,因为每个调用都需要在堆栈上管理一个额外的框架。谁知道内联是否可以抵消这一点。无论如何,这是极小的更新2 我现在已经对此进行了基准测试,并如我之前的更新所预测的那样:

"Queue.Synchronized" 比 "Queue+lock" 慢

我进行了单线程测试,因为它们都使用相同的锁定技术(即 lock),因此在“直线”中测试纯开销似乎是合理的。
我的基准测试针对一个发布版本生成了以下结果:
Iterations      :10,000,000

Queue+Lock      :539.14ms
Queue+Lock      :540.55ms
Queue+Lock      :539.46ms
Queue+Lock      :540.46ms
Queue+Lock      :539.75ms
SynchonizedQueue:578.67ms
SynchonizedQueue:585.04ms
SynchonizedQueue:580.22ms
SynchonizedQueue:578.35ms
SynchonizedQueue:578.57ms

使用以下代码:

private readonly object _syncObj = new object();

[Test]
public object measure_queue_locking_performance()
{
    const int TestIterations = 5;
    const int Iterations = (10 * 1000 * 1000);

    Action<string, Action> time = (name, test) =>
    {
        for (int i = 0; i < TestIterations; i++)
        {
            TimeSpan elapsed = TimeTest(test, Iterations);
            Console.WriteLine("{0}:{1:F2}ms", name, elapsed.TotalMilliseconds);
        }
    };

    object itemOut, itemIn = new object();
    Queue queue = new Queue();
    Queue syncQueue = Queue.Synchronized(queue);

    Action test1 = () =>
    {
        lock (_syncObj) queue.Enqueue(itemIn);
        lock (_syncObj) itemOut = queue.Dequeue();
    };

    Action test2 = () =>
    {
        syncQueue.Enqueue(itemIn);
        itemOut = syncQueue.Dequeue();
    };

    Console.WriteLine("Iterations:{0:0,0}\r\n", Iterations);
    time("Queue+Lock", test1);
    time("SynchonizedQueue", test2);

    return itemOut;
}

[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.GC.Collect")]
private static TimeSpan TimeTest(Action action, int iterations)
{
    Action gc = () =>
    {
        GC.Collect();
        GC.WaitForFullGCComplete();
    };

    Action empty = () => { };

    Stopwatch stopwatch1 = Stopwatch.StartNew();

    for (int j = 0; j < iterations; j++)
    {
        empty();
    }

    TimeSpan loopElapsed = stopwatch1.Elapsed;

    gc();
    action(); //JIT
    action(); //Optimize

    Stopwatch stopwatch2 = Stopwatch.StartNew();

    for (int j = 0; j < iterations; j++) action();

    gc();

    TimeSpan testElapsed = stopwatch2.Elapsed;

    return (testElapsed - loopElapsed);
}

如果原始问题没有足够积极地释放锁,或者过于急于获取锁,则会出现问题。 - jason
@Jason,我做出的假设是OP在EnqueueDequeue调用周围最小化使用锁定,但我理解你的观点。我在我的答案中进行了一些澄清。 - Tim Lloyd
感谢您投入时间和精力来获取这个问题的权威答案。向您致敬。 - Danish Khan
2
以上测试中的值与使用ConcurrentQueue相比如何? - Josh DeLong

6
我们无法为您回答这个问题。只有您自己可以通过获取一个分析器并在应用程序的真实数据上测试两种情况(Queue.Synchronized vs. Lock)来回答这个问题。它可能甚至不是应用程序中的瓶颈。
话虽如此,您应该只使用ConcurrentQueue

1
@Danish: 请查看Rx。它为.NET 3.5提供了ConcurrentQueue<T>的后移版本。 - Dan Tao
@Dan 那个版本的 ConcurrentQueue<T> 真的更快吗?我看过几次这样的建议,但没有实际的性能数据。有数据吗? - Tim Lloyd
@chibacity:你是在问它是否像.NET 4.0中的ConcurrentQueue<T>一样快,还是比由lock语句保护的普通香草Queue<T>更快?对于前者,我不知道。对于后者,这取决于争用的程度(如果几乎没有争用,则带锁的Queue<T>更好;随着潜在争用的增加,并发版本越来越领先)。如果您想要,我可能可以找到一些数字。 - Dan Tao
@Dan 我的主要观点是我知道 Rx(3.5)中有一个版本,但我猜想这不是与 .Net 4.0 中相同的版本。 - Tim Lloyd
@Dan 感谢你的探究,我知道 Rx 有这个功能,但我认为实现方式在那时和 .Net 4.0 之间可能已经发生了很大变化。而且你说得很对,在没有争用的情况下,“Queue+Lock”比“ConcurrentQueue<T>”更快。 - Tim Lloyd
显示剩余2条评论

0
  1. Queue.Synchronize 将一个新的同步队列进行封装,而 Lock Queue.SyncRoot 则提供了一个对象来以同步方式访问队列,因此您可以确保在使用线程同时执行 Enqueue 和 Dequeue 操作时,队列的线程安全性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接