线程安全的并发字典重新初始化

本文关键字:初始化 字典 并发 安全 线程 | 更新日期: 2023-09-27 18:17:26

我想知道下面的代码是否线程安全,我认为它不是。我怎么可能使它线程安全?

基本上我有一个ConcurrentDictionary作为数据库表的缓存。我想查询DB每10秒和更新DB缓存。整个过程中都会有其他线程查询这个字典。

我不能只使用TryAdd,因为也可能有我的元素已被删除。所以我决定不搜索整个字典来更新、添加或删除。我会重新初始化字典。如果这是一个愚蠢的想法,请告诉我。

我担心的是,当我重新初始化字典时,当初始化发生时,查询线程将不再是实例的线程安全。出于这个原因,我在更新字典时使用了锁,但是我不确定这是否正确,因为对象在锁中发生了变化?

private static System.Timers.Timer updateTimer;
private static volatile Boolean _isBusyUpdating = false;
private static ConcurrentDictionary<int, string> _contactIdNames;
public Constructor()
{
    // Setup Timers for data updater         
    updateTimer = new System.Timers.Timer();
    updateTimer.Interval = new TimeSpan(0, 0, 10, 0).TotalMilliseconds;
    updateTimer.Elapsed += OnTimedEvent;
    // Start the timer
    updateTimer.Enabled = true;
}
private void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e)
{
    if (!_isBusyUpdating)
    {
        _isBusyUpdating = true;
        // Get new data values and update the list
        try
        {
            var tmp = new ConcurrentDictionary<int, string>();
            using (var db = new DBEntities())
            {
                foreach (var item in db.ContactIDs.Select(x => new { x.Qualifier, x.AlarmCode, x.Description }).AsEnumerable())
                {
                    int key = (item.Qualifier * 1000) + item.AlarmCode;
                    tmp.TryAdd(key, item.Description);
                }
            }
            if (_contactIdNames == null)
            {
                _contactIdNames = tmp;
            }
            else
            {
                lock (_contactIdNames)
                {
                    _contactIdNames = tmp;
                }
            }
        }
        catch (Exception e)
        {
            Debug.WriteLine("Error occurred in update ContactId db store", e);
        }
        _isBusyUpdating = false;
    }
}
    /// Use the dictionary from another Thread
    public int GetIdFromClientString(string Name)
    {
        try
        {
            int pk;
            if (_contactIdNames.TryGetValue(Name, out pk))
            {
                return pk;
            }
        }
        catch { }
        //If all else fails return -1
        return -1;
    }

线程安全的并发字典重新初始化

你是对的,你的代码不是线程安全的。

  1. 需要锁定_isBusyUpdating变量
  2. 你需要每次锁定_contactIdNames,不只是当它不是null

这段代码也类似于单例模式,它在初始化时也有同样的问题。你可以通过双重检查锁定来解决这个问题。但是,在访问条目时还需要双重检查锁定。

在一次更新整个字典的情况下,每次访问时都需要锁定当前值。否则你可以在它还在变化的时候访问它,然后得到错误。因此,您要么每次都需要锁定变量,要么使用Interlocked

MSDN说volatile应该做_isBusyUpdating的技巧,它应该是线程安全的。

如果您不想跟踪_contactIdNames线程安全,请尝试在同一字典上实现每个条目的更新。问题将是在DB和当前值之间的差异检测(哪些条目已被删除或添加,其他条目可以简单地重写),但不是线程安全,因为ConcurrentDictionary已经是线程安全的。

你似乎为自己做了很多工作。下面是我处理这个任务的方法:

public class Constructor
{
    private volatile Dictionary<int, string> _contactIdNames;
    public Constructor()
    {
        Observable
            .Interval(TimeSpan.FromSeconds(10.0))
            .StartWith(-1)
            .Select(n =>
            {
                using (var db = new DBEntities())
                {
                    return db.ContactIDs.ToDictionary(
                        x => x.Qualifier * 1000 + x.AlarmCode,
                        x => x.Description);
                }
            })
            .Subscribe(x => _contactIdNames = x);
    }
    public string TryGetValue(int key)
    {
        string value = null;
        _contactIdNames.TryGetValue(key, out value);
        return value;
    }
}

我使用微软的响应式扩展(Rx)框架——NuGet"Rx- main"——作为计时器来更新字典。

Rx应该相当简单。如果你以前没有见过它,用非常简单的术语来说,它就像LINQ遇到事件。

如果你不喜欢Rx,那就使用你当前的计时器模型。

所有这些代码所做的是每10秒从DB创建一个新字典。我只是使用一个普通的字典,因为它只是从一个线程创建的。因为引用赋值是原子的,所以你可以在你喜欢的时候重新赋值字典,同时保证线程的完全安全。

只要元素不改变,多个线程可以安全地从字典中读取。

我想知道下面的代码是否线程安全,我假设是不是。我怎么可能使它线程安全?

我认为不是。首先,我会为ConcurrentDictionary创建属性,并检查get方法内是否正在进行更新,如果是,我会返回以前版本的对象:

    private object obj = new object();
    private ConcurrentDictionary<int, string> _contactIdNames;
    private ConcurrentDictionary<int, string> _contactIdNamesOld;
    private volatile bool _isBusyUpdating = false;
    public ConcurrentDictionary<int, string> ContactIdNames
    {
        get
        {
            if (!_isBusyUpdating) return _contactIdNames;
            return _contactIdNamesOld;
        }
        private set 
        {
            if(_isBusyUpdating) _contactIdNamesOld = 
                new ConcurrentDictionary<int, string>(_contactIdNames);
            _contactIdNames = value; 
        }
    }

你的方法可以是:

    private static void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e)
    {
        if (_isBusyUpdating) return;
        lock (obj)
        {
            _isBusyUpdating = true;
            // Get new data values and update the list
            try
            {
                ContactIdNames = new ConcurrentDictionary<int, string>();
                using (var db = new DBEntities())
                {
                    foreach (var item in db.ContactIDs.Select(x => new { x.Qualifier, x.AlarmCode, x.Description }).AsEnumerable())
                    {
                        int key = (item.Qualifier * 1000) + item.AlarmCode;
                        _contactIdNames.TryAdd(key, item.Description);
                    }
                }                    
            }
            catch (Exception e)
            {
                Debug.WriteLine("Error occurred in update ContactId db store", e);        
                _contactIdNames = _contactIdNamesOld;            
            }
            finally 
            {                   
                _isBusyUpdating = false;
            }
        }
    }

公立小学

我担心的是,当我重新初始化字典查询实例的线程将不再是线程安全的进行初始化。出于这个原因,我用了一把锁字典更新时,但我不确定这是否正确当物体在锁中改变时?

它的ConcurrentDictionary<T>类型是线程安全的,而不是它的实例,所以即使你创建了一个新的实例并改变了对它的引用-这是不需要担心的。