线程安全更新缓存的参考数据

时间:2014-02-21 19:14:47

标签: c# .net thread-safety

说我有几个List属性。像这样:

List<CustomerTypes> CustomerTypes {get; set;}
List<FormatTypes> FormatTypes {get; set;}
List<WidgetTypes> WidgetTypes {get; set}
List<PriceList> PriceList {get; set;}

因为这些值很少更新,所以我在启动时将它们缓存在我的WCF服务中。然后我有一个服务操作,可以调用它来刷新它们。

服务操作将从数据库中查询所有内容,如下所示:

// Get the data from the database.
var customerTypes = dbContext.GetCustomerTypes();
var formatTypes = dbContext.GetFormatTypes();
var widgetTypes = dbContext.GetWidgetTypes ();
var priceList = dbContext.GetPriceList ();

// Update the references
CustomerTypes = customerTypes;
FormatTypes = formatTypes;
WidgetTypes = widgetTypes;
PriceList = priceList;

这导致很少有时间这些并非全部同步。但是,它们并非完全线程安全。 (呼叫可以访问新的CustomerType和旧的PriceList。)

我如何才能这样做,以便在更新引用时,对这些列表的任何使用都必须等到所有引用都更新?

3 个答案:

答案 0 :(得分:4)

首先将所有这些列表放入一个容器类中。

Class TypeLists
{
    List<CustomerTypes> CustomerTypes {get; set;}
    List<FormatTypes> FormatTypes {get; set;}
    List<WidgetTypes> WidgetTypes {get; set}
    List<PriceList> PriceList {get; set;}
}

然后用函数调用替换旧的属性访问。

private readonly object _typeListsLookupLock = new object();
private volatile TypeLists _typeLists;
private volatile DateTime _typeListAge;

public TypeLists GetTypeList()
{
    if(_typeLists == null || DateTime.UtcNow - _typeListAge > MaxCacheAge)
    {
        //The assignment of _typeLists is thread safe, this lock is only to 
        //prevent multiple concurrent database lookups. If you don't care that 
        //two threads could call GetNewTypeList() at the same time you can remove 
        //the lock and inner if check.
        lock(_typeListsLookupLock)
        {
            //Check to see if while we where waiting to enter the lock someone else 
            //updated the lists and making the call to the database unnecessary.
            if(_typeLists == null || DateTime.UtcNow - _typeListAge > MaxCacheAge)
            {
                _typeLists = GetNewTypeList();
                _typeListAge = DateTime.UtcNow;
            }
        }
    }
    return _typeLists;
}

private TypeLists GetNewTypeList()
{
    var container = new TypeLists()
    using(var dbContext = GetContext())
    {
        container.CustomerTypes = dbContext.GetCustomerTypes();
        container.FormatTypes = dbContext.GetFormatTypes();
        container.WidgetTypes = dbContext.GetFormatTypes();
        container.PriceList = dbContext.GetPriceList ();
    }
    return container;
}

我们从属性更改为函数的原因是你做了

SomeFunction(myClass.TypeLists.PriceList, myClass.TypeLists.FormatTypes);

您可以在多线程环境中更改TypeLists,但是如果您这样做了

var typeLists = myClass.GetTypeLists();
SomeFunction(typeLists.PriceList, typeLists.FormatTypes);

typeLists对象在线程之间没有变异,所以你不必担心它的值会从你的下方改变,你可以做var typeLists = myClass.TypeLists但是使它成为一个函数使得它更清楚你可能会在通话之间得到不同的结果。

如果您想要表现出色,可以更改GetTypeList(),以便MemoryCache使用{{3}}来检测对象何时到期并获取新对象。

答案 1 :(得分:0)

我认为把一些东西放在一起就好了。这个答案是基于Marc Gravell的回答here的指导。

以下类接受毫秒值并提供    用于通知调用者刷新间隔已被命中的事件。

  • 它使用的Environment.TickCount要快几个数量级 而不是使用DateTime对象。

  • 双重检查锁可防止多个线程刷新 同时并从减少的开销中获益 锁定每次通话。

  • 使用Task.Run()刷新ThreadPool上的数据允许 调用者继续不使用现有的缓存数据。

    using System;
    using System.Threading.Tasks;
    
    namespace RefreshTest {
        public delegate void RefreshCallback();
    
        public class RefreshInterval {
            private readonly object _syncRoot = new Object();
    
            private readonly long _interval;
            private long _lastRefresh;
            private bool _updating;
    
            public event RefreshCallback RefreshData = () => { };
    
            public RefreshInterval(long interval) {
                _interval = interval;
            }
    
            public void Refresh() {
                if (Environment.TickCount - _lastRefresh < _interval || _updating) {
                    return;
                }
    
                lock (_syncRoot) {
                    if (Environment.TickCount - _lastRefresh < _interval || _updating) {
                        return;
                    }
    
                    _updating = true;
    
                    Task.Run(() => LoadData());
                }
            }
    
            private void LoadData() {
                try {
                    RefreshData();
    
                    _lastRefresh = Environment.TickCount;
                }
                catch (Exception e) {
                    //handle appropriately
                }
                finally {
                    _updating = false;
                }
            }
        }
    }
    

Interlocked提供了缓存数据的快速原子替换。

using System.Collections.Generic;

namespace RefreshTest {
    internal static class ContextCache {
        private static readonly RefreshInterval _refresher = new RefreshInterval(60000);
        private static List<int> _customerTypes = new List<int>();

        static ContextCache() {
            _refresher.RefreshData += RefreshData;
        }

        internal static List<int> CustomerTypes {
            get {
                _refresher.Refresh();

                return _customerTypes;
            }
        }

        private static void RefreshData() {
            List<int> customerTypes = new List<int>();  //dbContext.GetCustomerTypes();

            Interlocked.Exchange(ref _customerTypes, customerTypes);
        }
    }
}

几百万个并发调用运行~100ms(尽管运行自己的测试!):

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

namespace RefreshTest {
    internal class Program {
        private static void Main(string[] args) {
            Stopwatch watch = new Stopwatch();
            watch.Start();

            List<Task> tasks = new List<Task>();

            for (int i = 0; i < Environment.ProcessorCount; i++) {
                Task task = Task.Run(() => Test());

                tasks.Add(task);
            }

            tasks.ForEach(x => x.Wait());

            Console.WriteLine("Elapsed Milliseconds: {0}", watch.ElapsedMilliseconds);
            Console.ReadKey();
        }

        private static void Test() {
            for (int i = 0; i < 1000000; i++) {
                var a = ContextCache.CustomerTypes;
            }
        }
    }
}

希望有所帮助。

答案 2 :(得分:-1)

如果你有一个简单的场景可能,你可以使用 HACK

以编程方式编辑你的web.config(不重要的是你编辑的内​​容,你可以发明一个计数器,或者在一些发明的appSetting上从0到1或从1回到0)。

例如,查看here

这将允许所有现有请求完成,然后它将在IIS中重新启动您的应用程序域。 在新的app域开始时,来自db的数据将被重新加载到您的列表中。

请注意,您的新应用域的启动会有延迟(第一次请求,再次判断IL),您也会在会话,应用程序等中丢失数据。

优点是,在运行时,由于锁定而没有任何性能损失。