许多作者单一读者并发模型中最有效的锁是什么?

4

我有许多线程向我提供输入数据,必须按照到达顺序由单个线程处理。目前,所有的输入项都被插入到一个队列中,并且对队列的读/写操作受到C#锁语句的保护。然而,随着时间的推移,应用程序的CPU使用率上升到不可接受的水平,并且分析器显示大部分CPU时间都花在了锁语句本身上。是否有更有效的同步方法可以替代锁,支持多个写入者和一个读取者?

3个回答

4
似乎作者们正在为锁而争执。考虑一个模型,在该模型中,每个作者都有自己的队列,且读者使用Peek方法从每个队列中读取第一条消息而不将其移除。然后,读者可以在队列之间保持迭代,查看来自每个队列的第一项中的集合中的第一项,然后删除和处理该第一项。这比您当前的架构要慢,但应该消除作者之间的锁争用。
一个简单的示例可能如下所示:
public class TimestampedItem<T> : IComparable<TimestampedItem<T>>
{
    public DateTime TimeStamp { get; set; }
    public T Data { get; set; }
    public int CompareTo(TimestampedItem<T> other)
    {
        return TimeStamp.CompareTo(other.TimeStamp);
    }
}

public void ReadFirstFromEachQueue<T>(IEnumerable<Queue<TimestampedItem<T>>> queues)
{
    while (true)
    {
        var firstItems = new List<TimestampedItem<T>>(queues.Select(q => { lock (q) { return q.Peek(); } }));
            ProcessItem(firstItems.OrderBy(tsi => tsi.TimeStamp).First());
        }
    }
}

2
如果您使用的是 .net 版本 4.0,则可以使用 ConcurrentQueue(它是 ConcurrentCollections 的一部分)代替普通的 Queue,并在读/写队列数据时消除锁定。 ConcurrentCollections 旨在用于处理具有无锁代码的并发读/写。
如果您没有使用 4.0,则可以仅在没有其他锁定时锁定,这可以通过使用 Monitor.TryEnter 而不是 lock 来实现。请注意,lock 本身是 Monitor.EnterMonitor.Exit 的组合。示例实现如下:
private readonly object _syncObject = new object();

private bool TryUpdate(object someData)
{
    if (Monitor.TryEnter(_syncObject))
    {
        try
        {
            //Update the data here.

            return true;
        }
        finally
        {
            Monitor.Exit(_SyncObject);
        }
    }

    return false;
}

Monitor.TryEnter相比于Monitor.Enter的优势是什么? - GWLlosa
如果lock正在使用,第一个"Monitor.TryEnter"不会阻塞等待进入,而是会返回false,而不是在该行等待它被释放。 - Jalal Said

1

这可能对您的应用程序是一个很大的改变,但您可以考虑将队列外部化到应用程序之外(例如 MSMQ),然后您的写入线程可以尽情地向该队列写入。当准备好时,读取器只需取出项目即可。如果您的大部分 CPU 时间仅用于锁定队列周围(我假设您实际上没有在放置在队列上的项目上进行锁定),那么将队列放置在应用程序之外可能会真正有所帮助。理想情况下,您还可以将编写和阅读拆分为单独的进程。

另一件要检查的事情是,您正在锁定的对象是否被用于在应用程序中的其他位置进行锁定。监视器(锁语句背后的东西)可能是最轻量级的线程同步方法,因此最好重新设计架构以避免在执行项目处理的同一进程中进行锁定。


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