天天看點

C# 應用 - 多線程 7) 處理同步資料之 Synchronized code regions (同步代碼區域): Monitor 和 lock

目錄:

  1. System.Threading.Monitor:提供同步通路對象的機制;
  2. lock 是文法糖,是對 Monitor Enter 和 Exit 方法的一個封裝
  3. lock 案例

1. Monitor

1. 基本方法

  1. public static void Enter(object obj);

    在指定對象上擷取排他鎖。

  2. public static void Exit(object obj);

    釋放指定對象上的排他鎖。

2. 使用例子

// 被 Monitor 保護的隊列
private Queue<T> m_inputQueue = new Queue<T>();

// 給 m_inputQueue 加鎖,并往 m_inputQueue 添加一個元素
public void Enqueue(T qValue)
{
  // 請求擷取鎖,并阻塞其他線程獲得該鎖,直到獲得鎖
  Monitor.Enter(m_inputQueue);
  try
  {
     m_inputQueue.Enqueue(qValue);
  }
  finally
  {
     // 釋放鎖
     Monitor.Exit(m_inputQueue);
  }
}
           

2. lock

lock 是文法糖,是對Monitor的Enter和Exit的一個封裝。

lock (m_inputQueue) {} 等價于

bool __lockWasTaken = false;
try
{
    System.Threading.Monitor.Enter(m_inputQueue, ref __lockWasTaken);
}
finally
{
    if (__lockWasTaken) System.Threading.Monitor.Exit(m_inputQueue);
}
           
  1. 當同步對共享資源的線程通路時,請鎖定專用對象執行個體(例如,private readonly object balanceLock = new object();)或另一個不太可能被代碼無關部分用作 lock 對象的執行個體。 避免對不同的共享資源使用相同的 lock 對象執行個體,因為這可能導緻死鎖或鎖争用;
  2. 具體而言,避免将以下對象用作 lock 對象:

    1)this(調用方可能将其用作 lock)

    2)Type 執行個體(可以通過 typeof 運算符或反射擷取)

    3)字元串執行個體,包括字元串文本,(這些可能是暫存的)。

    盡可能縮短持有鎖的時間,以減少鎖争用。

private readonly object balanceLock = new object();
private Queue<T> m_inputQueue = new Queue<T>();

public void Enqueue(T qValue)
{
    lock (balanceLock)
    {
        m_inputQueue.Enqueue(qValue);
    }
}
           

3. lock 案例

1. 資料庫通路工廠單例模式

private static object _iBlockPortLockObj = new object();
private static IBlockPort _iBlockPort;

/// <summary>
/// 卡口
/// </summary>
/// <returns></returns>
public static IBlockPort CreateBlockPort()
{            
    if (_iBlockPort == null)
    {
        lock (_iBlockPortLockObj)
        {
            if (_iBlockPort == null)
            {
                string className = AssemblyName + "." + db + "BlockPort";
                _iBlockPort = (IBlockPort)Assembly.Load(AssemblyName).CreateInstance(className);
            }
        }                
    }

    return _iBlockPort;
}
           

2. 隊列進出

public abstract class AbstractCache<T> where T : ICloneable
{
    protected int queenLength = 30; // 保持隊列的最大長度,主要可能考慮記憶體

    /// <summary>
    /// 過車緩存清單
    /// </summary>
    public List<T> listCache { get; set; }

    protected object _lockObj = new object();

    /// <summary>
    /// 初始化或重置緩存清單
    /// </summary>
    protected void RefreshListCache()
    {
        lock (_lockObj)
        {
            if (listCache == null)
            {
                listCache = new List<T>();
            }
            else
            {
                listCache.Clear();
            }
        }            
    }
    
    /// <summary>
    /// 添加新的資料進隊列,後續考慮做成環形隊列減少開銷
    /// </summary>
    /// <param name="list"></param>
    protected void AddListToCache(List<T> list)
    {
        lock (_lockObj)
        {
            if (listCache == null) return;

            listCache.InsertRange(0, list);
            if (listCache.Count > queenLength)
            {
                listCache.RemoveRange(queenLength, listCache.Count - queenLength);
            }
        }
    }

    /// <summary>
    /// 移除并傳回過車緩存隊列的最後一個元素
    /// </summary>
    /// <returns></returns>
    public T DequeueLastCar()
    {
        T res = default;

        lock (_lockObj)
        {
            if (listCache != null && listCache.Count > 0)
            {
                int lastIndex = listCache.Count - 1;
                res = (T)listCache[lastIndex].Clone();
                listCache.RemoveAt(lastIndex);
            }
        }

        return res;
    }
}
           
  1. 前提:在某項目上,view 的控件包括一個下拉框(可選idA、idB等)、一個圖檔 image;
  2. 資料邏輯設計:線程 A 定時根據下拉框的選擇作為條件從第三方的資料庫擷取資料并添加進隊列

    1)線程 B 定時從隊列取出一個并展示到 image 控件

    2)當下拉框切換選擇時,清空隊列 [便于展示跟下拉框關聯的圖檔]

  3. 問題:從第三方的資料庫取資料需要 1s 左右,如果剛好出現這樣的操作:線程 A 查資料庫擷取 idA 相關的資料(将持續 1s)-> 下拉框 idA 切換到 idB 并觸發執行清空隊列操作 -> 線程 A 将 idA 的資料添加到隊列,将會出現下拉框切換 idB 之後依舊展示 idA 相關的資料。
  4. 解決:線上程 a 查資料庫時就對隊列加鎖(同時去掉隊列入隊的鎖,避免死鎖),這樣在擷取資料的中途切換下拉框,就能等到擷取完并加入隊列後再清空。
  5. 導緻新的問題:在擷取的過程中,因隊列被鎖,導緻無法線程 B 出隊的操作被阻塞。
  6. 解決:入隊和出隊共用一個鎖,從資料庫擷取資料和清空隊列共用一個鎖。
/// <summary>
/// 添加新的資料進隊列,後續考慮做成環形隊列減少開銷
/// 清空、添加、取出一個資料,都需要加鎖,但是由于添加的資料是從海康那邊拿過來的,可能需要幾秒的時間,        
/// 可能會導緻這樣的結果:線程 A 查資料庫(持續幾秒)-> 線程 B 執行清空隊列操作 -> 線程 A 将資料添加到隊列
/// 是以将,鎖直接移動到 lock {線程 A 查資料庫、将資料添加到隊列}
/// </summary>
/// <param name="list"></param>
protected void AddListToCache(List<T> list)
{
    if (listCache == null) return;

    listCache.InsertRange(0, list);
    if (listCache.Count > queenLength)
    {
        listCache.RemoveRange(queenLength, listCache.Count - queenLength);
    }
}

CancellationTokenSource source = new CancellationTokenSource();

/// <summary>
/// 定時擷取 xx 資料
/// </summary>
public void GetPassCarInterval()
{
    Task.Factory.StartNew(() =>
    {
        while (!source.IsCancellationRequested)
        {
            if (!string.IsNullOrWhiteSpace(xx))
            {
                lock (_lockObj)
                {
                    // 從資料庫擷取資料
                    var list = GetPassCarInfo.GetLastBlockPortCarRecordBy(xx);
                    
                    AddListToCache(list);
                }                        
            }                    

            AutoReset.WaitOne(Common.GetDataTimespan);
        }
    }, TaskCreationOptions.LongRunning);
}