簡體   English   中英

如何提高兩個線程同時訪問項目集合的性能

[英]How to improve performance on two thread accesing a collection of items concurrently

我正在.net項目3.5上實現Producer / Consumer方法

只有一個生產者和一個消費者在各自的線程上運行

CheckOrderToProcess方法檢查符合某些條件的表並將其添加到列表中(生產者)

bgOrdenes_DoWork方法獲取列表中的每個項目並執行一些邏輯(消費者)

我想避免鎖定整個列表lstOrderToProcessto以提高性能,我嘗試使用ConcurrentQueue但由於無法在無法升級到4.0的項目中使用而受到限制,因此我無法升級到.net 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
            }
        }
    }
}

沒有可能的原因來更改此代碼段中的鎖定。 但是,它確實存在一些令人討厭的設計缺陷:

  • DoWork內部的等待循環是一個熱等待循環 它消耗100%的內核,99.99999%的時間不完成任何操作。 通常,這對運行此代碼的計算機非常不友好。 即使您在嘗試檢測大量CPU周期時也將導致程序對添加的項目無響應。 刻錄完量子后,操作系統的線程調度程序會將您保留在狗屋中一段時間​​。

  • 待定變量用作同步對象,但僅僅是一個簡單的布爾變量。 當您這樣做時,很多事情都會出錯。 一方面,很有可能您的代碼在運行代碼的Release版本時從未看到變量設置為true。 在32位代碼中存在問題,此類變量必須聲明為volatile 這也是低效的,線程可能需要一段時間才能觀察到分配的值。

  • lstOrderToProcess.Any()的使用效率低下。 實際上,僅刪除索引0處的元素將毫無意義地清空整個列表。

  • 使用者在BackgroundWorker上運行。 它使用線程池線程來實現工作程序。 TP線程通常不應運行超過半秒。 但是,您的線程將永遠運行,嚴重阻礙了線程池調度程序的工作,以使待執行的tp線程請求得到執行。 這會對整個應用程序的響應性產生負面影響。

通過使用常規線程運行使用者來取得成功。 為列表使用更好的設計,您需要Blocking Queue 通過使用線程管理大師提供的代碼,您可以獲得在舊.NET版本上運行的代碼,Joe Duffy的示例實現解決了您的熱點等待循環問題。 我將其重新發布在此處,進行了調整,使其不等待消費者:

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;
    } 
}

如果我要在您的情況下做生產者/消費者,那么我也將從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是安全的),但是如果數組太小或使用者太慢,則沒有空插槽時,您必須處理這種情況。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM