以线程安全的方式更改和读取ConcurrentDictionary中对象的属性

时间:2017-11-01 14:15:08

标签: c# multithreading concurrency

我使用ConcurrentDictionary在web api应用程序中收集内存中的数据。使用api方法,我在ConcurrentDictionary中添加和更新对象。还有后台线程根据对象属性分析和清理这个字典。现在我正在考虑两种方法:
1。updateValueFactory方法的AddOrUpdate中对字典项使用锁,但问题是如何正确读取属性以确保我有最新版本的那个和我'我不是在不稳定的状态下阅读财产

public class ThreadsafeService2
{
    private readonly ConcurrentDictionary<string, ThreadSafeItem2> _storage = 
        new ConcurrentDictionary<string, ThreadSafeItem2>();

    public void AddOrUpdate(string name)
    {
        var newVal = new ThreadSafeItem2();
        _storage.AddOrUpdate(name, newVal, (key, oldVal) =>
        {
            //use lock
            lock (oldVal)
            {
                oldVal.Increment();
            }
            return oldVal;
        });
    }

    public void Analyze()
    {
        foreach (var key in _storage.Keys)
        {
            if (_storage.TryGetValue(key, out var item))
            {
                //how to read it properly?
                long ticks = item.ModifiedTicks;
            }
        }
    }
}
public class ThreadSafeItem2
{
    private long _modifiedTicks;
    private int _counter;
    public void Increment()
    {
        //no interlocked here
        _modifiedTicks = DateTime.Now.Ticks;
        _counter++;
    }

    //now interlocked here
    public long ModifiedTicks => _modifiedTicks;
    public int Counter => _counter;
}

2。在没有锁定的情况下在属性级别使用Interlockedmemory barriers,对我来说看起来有点冗长

public class ThreadsafeService1
    {
            private readonly ConcurrentDictionary<string, ThreadSafeItem1> _storage = 
                new ConcurrentDictionary<string, ThreadSafeItem1>();

            public void AddOrUpdate(string name)
            {
                var newVal = new ThreadSafeItem1();
                _storage.AddOrUpdate(name, newVal, (key, oldVal) =>
                {
                    //no lock here
                    oldVal.Increment();
                    return oldVal;
                });
            }

            public void Analyze()
            {
                foreach(var key in _storage.Keys)
                {
                    if(_storage.TryGetValue(key, out var item))
                    {
                        //reading through interloacked
                        long ticks = item.ModifiedTicks;
                    }
                }
            }
        }

        public class ThreadSafeItem1
        {
            private long _modifiedTicks;
            private int _counter;
            public void Increment()
            {
                //make changes in atomic manner
                Interlocked.Exchange(ref _modifiedTicks, DateTime.Now.Ticks);
                Interlocked.Increment(ref _counter);
            }

            public long ModifiedTicks => Interlocked.Read(ref _modifiedTicks);
            public int Counter => Thread.VolatileRead(ref _counter);
        }

这里的最佳做法是什么?

2 个答案:

答案 0 :(得分:0)

因此,您的两个实现都存在重大问题。第一个解决方案在递增时锁定,但在读取时不锁定,这意味着访问数据的其他位置可以读取无效状态。

非技术性问题,但一个主要问题是,您已将您的班级命名为ThreadSaveItem,但实际上它并非设计为可以安全地从多个线程访问。在此实现中,调用者责任是确保不从多个线程访问该项目。如果我看到一个名为ThreadSafeItem的课程,我会认为可以安全地从多个主题访问它,并且我不需要同步我对它的访问权限。我执行的每个操作都是唯一需要逻辑原子的操作。

您的Interlocked解决方案存在问题,因为您必须使用您正在修改的字段,这些字段在概念上绑定在一起,但您不能将其更改同步,这意味着有人可以观察到修改一个而不是另一个,这是该代码的问题。

接下来,在两种解决方案中使用AddOrUpdate并不合适。方法调用的重点是添加一个项目或用另一个项目替换它,而不是改变提供的项目(这就是为什么它需要一个返回值;你应该生成一个新项目)。如果你想采用获取可变项并改变它的方法,那么可以通过调用GetOrAdd来获取现有项或创建一个新项,然后在一个线程中对其进行变异使用返回值的安全方式。

只需简单地ThreadSafeItem不可变,就可以彻底简化整个解决方案。它允许您在AddOrUpdate上使用ConcurrentDictionary进行更新,这意味着需要完成的唯一同步是更新ConcurrentDictionary的值,并且它已经处理同步它自己的状态,在访问ThreadSafeItem时根本不需要同步,因为对数据的所有访问都是固有的线程安全,因为它是不可变的。这意味着您根本不需要编写任何同步代码,这正是您希望尽可能争取的。

最后,我们有实际的代码:

public class ThreadsafeService3
{
    private readonly ConcurrentDictionary<string, ThreadSafeItem3> _storage =
        new ConcurrentDictionary<string, ThreadSafeItem3>();

    public void AddOrUpdate(string name)
    {
        _storage.AddOrUpdate(name, _ => new ThreadSafeItem3(), (_, oldValue) => oldValue.Increment());
    }

    public void Analyze()
    {
        foreach (var pair in _storage)
        {
            long ticks = pair.Value.ModifiedTicks;
            //Note, the value may have been updated since we checked; 
            //you've said you don't care and it's okay for a newer item to be removed here if it loses the race.
            if (isTooOld(ticks))
                _storage.TryRemove(pair.Key, out _);  
        }
    }
}

public class ThreadSafeItem3
{
    public ThreadSafeItem3()
    {
        Counter = 0;
    }
    private ThreadSafeItem3(int counter)
    {
        Counter = counter;
    }
    public ThreadSafeItem3 Increment()
    {
        return new ThreadSafeItem3(Counter + 1);
    }

    public long ModifiedTicks { get; } = DateTime.Now.Ticks;
    public int Counter { get; }
}

答案 1 :(得分:-1)

我不能评论你究竟在做什么的具体细节,但是互锁和并发字典比你自己做的锁更好。

我会质疑这种方法。您的数据非常重要,但坚持下去并不是那么重要?根据应用程序的使用情况,这种方法会在一定程度上减慢速度。再一次,不知道你在做什么,你可以把每个“添加”扔进一个MSMQ,然后在一些时间表运行外部exe来处理这些项目。该网站只会触发并忘记,没有线程要求。