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

时间:2015-10-12 08:43:21

标签: c# multithreading locking concurrentdictionary

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

基本上我有一个ConcurrentDictionary,它充当数据库表的缓存。我想每10秒查询一次数据库并更新数据库缓存。将有其他线程一直查询这本字典。

我不能只使用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;
    }

3 个答案:

答案 0 :(得分:2)

你似乎为自己做了很多工作。以下是我将如何解决这个问题:

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

我正在使用Microsoft的Reactive Extensions(Rx)框架 - NuGet“Rx-Main” - 用于更新字典的计时器。

Rx应该相当简单。如果您之前没有以非常简单的术语看过它,就像LINQ遇到事件一样。

如果您不喜欢Rx,那么请使用您当前的计时器型号。

所有这些代码都是每隔10秒从数据库创建一个新字典。我只是使用普通字典,因为它只是从一个线程创建的。由于引用赋值是原子的,因此您可以在需要时使用完整的线程安全性重新分配字典。

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

答案 1 :(得分:1)

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

  1. 您需要锁定_isBusyUpdating变量。
  2. 您每次都需要锁定_contactIdNames,而不仅仅是null
  3. 此代码与singleton pattern类似,初始化时也存在同样的问题。您可以使用Double checked locking解决此问题。但是,访问条目时还需要双重检查锁定。

    如果您一次更新整个字典,则每次访问时都需要锁定当前值。否则,您可以在它仍在更改时访问它并获得错误。因此,您需要每次锁定变量或使用Interlocked

    由于MSDN says volatile应该使用_isBusyUpdating来处理,它应该是线程安全的。

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

答案 2 :(得分:0)

  

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

我相信不是。首先,我为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;
            }
        }
    }

P.S。

  

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

ConcurrentDictionary<T>类型是线程安全的而不是它的实例,所以即使你创建新实例并更改它的引用 - 也不用担心。