如何提高两个线程同时访问项集合的性能

时间:2013-12-18 14:21:57

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

我正在.net项目3.5上实施Producer / Consumer aproach

只有一个生产者和一个消费者各自在自己的线程上运行

CheckOrderToProcess方法检查符合特定条件的表并将它们添加到列表中(生产者)

bgOrdenes_DoWork方法获取列表中的每个项目并执行一些逻辑(消费者)

我想避免锁定整个列表lstOrderToProcessto以提高性能,我尝试使用ConcurrentQueue但由于将要使用的限制我无法升级到.net 4.0在无法升级到4.0的项目中

如何更改此实现以提高性能?我不必是一个列表,只要它可以在线程之间共享,我可以在最后添加并获取第一个元素(队列)

public class DatabaseCache : ICacheDB
{
    private static List<NotificacionOrdenes> lstOrderToProcess;
    private static Object lockOrders;
    private static BackgroundWorker bgOrdenes;
    private static bool pendingOrders = false;
    private static Timer timerCheck;

    public DatabaseCache(string eventSourceName, bool monitorearOrderNotifications)
    {
        bgOrdenes = new BackgroundWorker();
        bgOrdenes.DoWork += new DoWorkEventHandler(bgOrdenes_DoWork);

        lstOrderToProcess = new List<NotificacionOrdenes>();
        lockOrders = new Object();

        CheckOrderToProcess();

        if (!bgOrdenes.IsBusy)
        {
            bgOrdenes.RunWorkerAsync();
        }

        //execute CheckOrderToProcess periodically
        timerCheck = new Timer(2000);
        timerCheck.Elapsed += new ElapsedEventHandler(timerCheck_Elapsed);
        timerCheck.Enabled = true;
        timerCheck.AutoReset = true;

    }
    void timerCheck_Elapsed(object sender, ElapsedEventArgs e)
    {
        CheckOrderToProcess();
    }

    private void CheckOrderToProcess()
    {
        DataSet ds;
        NotificacionOrdenes notif;

        ds = Data.GetOrderNotifications_ToProcess();

        //if there is new info to process
        if ((ds != null) && (ds.Tables[0].Rows.Count != 0))
        {

            foreach (DataRow row in ds.Tables[0].Rows)
            {
                notif = new NotificacionOrdenes();

                //fill NOTIF with info of each row

                lock (lockOrders)
                {
                    lstOrderToProcess.Add(notif);
                }

            }

            pendingOrders = true;
        }
    }

    void bgOrdenes_DoWork(object sender, DoWorkEventArgs e)
    {
        NotificacionOrdenes notif;

        while (true)
        {
            if (pendingOrders)
            {
                lock (lockOrders)
                {
                    notif = lstOrderToProcess[0];
                    lstOrderToProcess.RemoveAt(0);

                    //check to see if there is any pending order
                    pendingOrders = lstOrderToProcess.Any();
                }

                //Execute rest of the logic
            }
        }
    }
}

2 个答案:

答案 0 :(得分:1)

在此代码段中无法改变锁定的可能原因。然而,它确实遭受了相当令人讨厌的设计缺陷:

  • DoWork内部的等待循环是热等待循环。它燃烧100%核心,99.99999%的时间没有任何成就。这通常对您运行此代码的计算机非常不友好。它还会导致您的程序对添加的项目没有响应,即使您在尝试检测时也会烧掉大量的cpu周期。在你烧掉量子之后,操作系统的线程调度程序会让你进入狗屋一段时间。

  • pendingOrders 变量用作同步对象,但只是一个简单的 bool 变量。当你这样做时,很多事情都会出错。首先,当您运行代码的Release版本时,您的代码永远不会将变量设置为true,这是很有可能的。在32位代码中存在问题,必须将此类变量声明为 volatile 。它也是低效的,在线程可以观察到指定的值之前可能需要一段时间。

  • 使用lstOrderToProcess.Any()是低效的。无论如何,只要删除索引0处的元素,无论如何都无法清空整个列表。

  • 消费者在BackgroundWorker上运行。它使用线程池线程来实现worker。 TP线程通常不应运行超过半秒钟。但是你的线程会永远运行,严重阻碍了线程池调度程序的工作,以获得待执行的待处理tp线程请求。这会对整个应用的响应能力产生负面影响。

通过使用常规线程来运行消费者来获得成功。并为列表使用更好的设计,您需要阻止队列。你可以使用线程大师的代码获得在旧.NET版本上运行的版本,Joe Duffy的sample implementation解决了你的热等待循环问题。我会在这里重新发布,调整为不等消费者:

public class BlockingQueue<T> { 
    private Queue<Cell<T>> m_queue = new Queue<Cell<T>>(); 
    public void Enqueue(T obj) { 
        Cell<T> c = new Cell<T>(obj);
        lock (m_queue) {
            m_queue.Enqueue(c); 
            Monitor.Pulse(m_queue); 
        }
    } 
    public T Dequeue() { 
        Cell<T> c; 
        lock (m_queue) { 
            while (m_queue.Count == 0) Monitor.Wait(m_queue); 
            c = m_queue.Dequeue();
        } 
        return c.m_obj; 
    } 
}

class Cell<T> { 
    internal T m_obj; 
    internal Cell(T obj) { 
        m_obj = obj;
    } 
}

答案 1 :(得分:0)

如果我在你的情况下做生产者/消费者,那么我也会从List<>开始。

但正如我现在看到的那样,这不是最好的情况,因为从列表中删除项目会导致索引被更改(这需要锁定整个列表)。

也许您可以使用数组代替?像这样:

NotificacionOrdenes[] todo = new NotificacionOrdenes[1000];

// producer
// find empty slot (null) in todo somehow
todo[empty_slot] = new NotificacionOrdenes();
...

// consumer
// find non-empty slot somehow
var notif = todo[non_empty_slot]
todo[non_empty_slot] = null;
..

正如您所看到的那样,不需要锁定(检查null并设置为null是安全的),但是当没有空槽,数组太小或消费者太慢时,您必须处理情况。