消息队列思考

时间:2011-01-18 19:56:53

标签: c# .net .net-3.5 queue

我们使用C#Queue实现了一个消息队列。我们知道我们只有一个消费者可以从队列中获取可用消息,以便使用while循环进行处理。我们也知道只有一个生产者将消息放入队列。

我们在上面的消息队列中有一个lock,以确保消费者和生产者不能同时访问队列。

我的问题是lock是否必要?如果Queue增加其Count属性 AFTER ,则实际添加了一个项目,如果消费者在检索之前检查Count,则消费者应该获得完整的消息项目即使我们没有lock。对?因此,我们不会面临部分邮件项目问题。然后我们可以摆脱lock

lock会降低系统的速度,偶尔我们可以看到检索线程暂时被阻止,因为我们有一个非常繁重的生产者。

修改

不幸的是我们正在使用.Net 3.5。

7 个答案:

答案 0 :(得分:10)

真正的问题是队列的内部数据结构在发生入队或出队时可能会发生变异,在此期间,数据结构处于与另一个线程的观点不确定的状态。 / p>

例如,入队可能需要扩展内部数据结构,其中创建新结构,旧项目从旧结构复制到新结构。此过程将涉及许多步骤,其中任何时候由于操作未完成而导致另一个线程访问队列将是危险的。

因此,在enqueue \ dequeue期间,您必须锁定以使这些操作看起来具有逻辑原子性。

您可以在.Net 4.0中尝试新的ConcurrentQueue类,因为它可能具有更好的性能特征,因为它使用非锁定算法。

答案 1 :(得分:4)

如果您使用的是Queue<T>,则需要锁定。您可以使用ConcurrentQueue<T>替换它来轻松删除它,但是,您可能需要考虑通过将其替换为BlockingCollection<T>来简化此代码。

这将允许您的消费者在检查时消除锁定,并且只需在collection.GetConsumingEnumerable()上执行一次预告。生产者可以消除锁定并根据需要添加项目。它也很容易让你扩展到使用多个生产者,因为你提到你现在有一个“非常沉重的生产者”。

答案 2 :(得分:2)

请参阅ConcurrentQueue

答案 3 :(得分:1)

不,这不会一直有效......为什么?

让我们反汇编我们将要从两个线程(一个读取和一个写入器)同时调用的两个方法:

public T Dequeue()
{
    if (this._size == 0)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EmptyQueue);
    }
    T local = this._array[this._head];
    this._array[this._head] = default(T);
    this._head = (this._head + 1) % this._array.Length;
    this._size--;
    this._version++;
    return local;
}

public void Enqueue(T item)
{
    if (this._size == this._array.Length)
    {
        int capacity = (int) ((this._array.Length * 200L) / 100L);
        if (capacity < (this._array.Length + 4))
        {
            capacity = this._array.Length + 4;
        }
        this.SetCapacity(capacity);
    }
    this._array[this._tail] = item;
    this._tail = (this._tail + 1) % this._array.Length;
    this._size++;
    this._version++;
}

鉴于上述代码,如果(并且仅当)队列中有足够的容量,则三个变量是安全的。 _array,_head和_tail的字段未经修改或仅在上述两种方法中的一种中进行修改。

你无法删除lock()的原因是两个方法都修改了_size和_version。虽然可以忽略_version上的碰撞,但是_size上的碰撞会引起一些不希望的和不可预测的行为。

答案 4 :(得分:1)

BTW,单个阅读器和单个编写器的无锁队列很容易编写。这是该概念的一个非常原始的例子,但它完成了这项工作:

class LocklessQueue<T>
{
    class Item
    {
        public Item Next;
        bool _valid;
        T _value;
        public Item(bool valid, T value)
        {
            _valid = valid;
            _value = value;
            Next = null;
        }
        public bool IsValid { get { return _valid; } }
        public T TakeValue()
        {
            T value = _value;
            _valid = false;
            _value = default(T);
            return value;
        }
    }

    Item _first;
    Item _last;

    public LocklessQueue()
    {
        _first = _last = new Item(false, default(T));
    }

    public bool IsEmpty
    { 
        get
        {
            while (!_first.IsValid && _first.Next != null)
                _first = _first.Next;
            return false == _first.IsValid;
        }
    }

    public void Enqueue(T value)
    {
        Item i = new Item(true, value);
        _last.Next = i;
        _last = i;
    }

    public T Dequeue()
    {
        while (!_first.IsValid && _first.Next != null)
            _first = _first.Next;

        if (IsEmpty)
            throw new InvalidOperationException();//queue is empty

        return _first.TakeValue();
    }
}

答案 5 :(得分:0)

你应该锁定;这个类不是线程安全的。如果您使用Queue中的System.Collections,则会有一个线程安全的Queue便捷(System.Collections.Queue.Synchronized()返回此类Queue)。否则,请务必使用提供的对象Queue<T>.SyncRoot进行同步:

using System.Collections.Generic;
public static class Q_Example
{
    private readonly Queue<int> q = new Queue<int>();
    public void Method1(int val)
    {
        lock(q.SyncRoot)
        {
            q.EnQueue(val);
        }
    }

    public int Method2()
    {
        lock(q.SyncRoot)
        {
            return q.Dequeue();
        }
    }
}

答案 6 :(得分:0)

即使您使用的是.NET 3.5,也可以使用

ConcurrentQueueReactive Extensions包括以前的.NET 3.5并行扩展 - .NET 4.0中包含的任务并行库的前身。