2014-02-21 3 views
5

Скажем, у меня есть несколько свойств списка. Нечто подобное:Обновление потокового кэша в кэше Справочные данные

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 и старому прайс-листу.)

Как я могу это сделать так, что, хотя я обновляю ссылки, любое использование этих списков должно ждать, пока все ссылки не будут обновлены?

+3

Используйте своего рода примитив синхронизации, например 'lock' или' ReaderWriterLockSlim'. –

ответ

0

Я подумал, что было бы здорово собрать что-то вместе в качестве примера. Этот ответ основан на рекомендации от Марка Гравелла here.

Следующий класс принимает значение в миллисекундах и предоставляет событие , чтобы уведомить вызывающего абонента о том, что интервал обновления был поражен.

  • Он использует Environment.TickCount, которая на несколько порядков быстрее, чем использование объектов DateTime.

  • Блокировка с двойной проверкой предотвращает одновременное обновление нескольких потоков от и дает возможность уменьшить накладные расходы, избегая блокировки при каждом звонке.

  • Обновление информации о ThreadPool с помощью Task.Run() позволяет вызывающему абоненту продолжать непрерывную работу с существующими кэшированными данными.

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

Несколько миллионов одновременных вызовов работает ~ 100мс (запустить свои собственные тесты, хотя!):

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

Надежда, что помогает.

+0

Это эпический прохладный ответ для автоматического обновления кэшированных данных. Однако, похоже, это не помогает с вызовом «Вызов», который может получить доступ к новой проблеме CustomerType и старой прайс-листу. (Он является атомарным для одного кэшированного списка, но мне нужно, чтобы он был атомарным для всех кешированных списков одновременно (так что старый кеш из одного и нового кеша из другого не может быть получен одним и тем же потоком. +1 для очень крутой ответ (я могу использовать), но не совсем то, что я искал. – Vaccano

+0

Ах, я пропустил эту часть, мой плохой. Скотт решил бросить их под один класс. – hyru

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, чтобы определить, когда он должен истекать и получить новый объект.

-1

Если у вас есть простой сценарий возможно вы можете использовать HACK.

Программно отредактируйте файл web.config (не важно, что вы редактируете, вы можете придумать счетчик или перейти от 0 до 1 или вернуться с 1 на 0 на какой-то придуманный appSetting).

Посмотрите, например, here.

Это позволит завершить все существующие запросы, а затем перезапустить домен вашего приложения в IIS. При запуске новых данных домена приложения данные из db будут перезагружены в ваши списки.

быть предупрежден, что your'll также испытывают задержку на запуск нового домена приложения (на 1-ом запросе, снова jitting IL), и вы также потеряете свои данные в сессии, заявки и т.д.

Преимущество в том, что при запуске у вас нет удара по производительности из-за блокировки.

+0

-1: что заставляет вас думать OP использует ASP.NET? –

+0

«Служба WCF при запуске» в вопросе OP.Я предполагаю, что он не является самостоятельным хостингом. Что заставляет вас не использовать ASP.NET? –

+0

Хотелось бы узнать кое-что Итак, если кто-то думает, что этот ответ плох, вы можете объяснить, почему это плохо? Не просто дать -1s. –

Смежные вопросы