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);
}
posted @ 2021-03-11 21:43  鑫茂  阅读(348)  评论(0编辑  收藏  举报