2016-06-02 4 views
0

я реализовал простой кэш-памяти, поддержанной ConcurrentDictionaryПочему значение, возвращаемое из ConcurrentDictionary, всегда равно null, когда используется несколько одновременных потоков?

public class MemoryCache 
{ 
    private ConcurrentDictionary<string, CacheObject> _memory; 

    public MemoryCache() 
    { 
     this._memory = new ConcurrentDictionary<string, CacheObject>(); 
    } 


    private bool TryGetValue(string key, out CacheObject entry) 
    { 
     return this._memory.TryGetValue(key, out entry); 
    } 

    private bool CacheAdd(string key, object value, DateTime? expiresAt = null) 
    { 
     CacheObject entry; 
     if (this.TryGetValue(key, out entry)) return false; 

     entry = new CacheObject(value, expiresAt); 
     this.Set(key, entry); 

     return true; 

    } 

    public object Get(string key) 
    { 
     long lastModifiedTicks; 
     return Get(key, out lastModifiedTicks); 
    } 

    public object Get(string key, out long lastModifiedTicks) 
    { 
     lastModifiedTicks = 0; 
     CacheObject CacheObject; 
     if (this._memory.TryGetValue(key, out CacheObject)) 
     { 
      if (CacheObject.HasExpired) 
      { 
       this._memory.TryRemove(key, out CacheObject); 
       return null; 
      } 
      lastModifiedTicks = CacheObject.LastModifiedTicks; 
      return CacheObject.Value; 
     } 
     return null; 
    } 

    public T Get<T>(string key) 
    { 
     var value = Get(key); 
     if (value != null) return (T)value; 
     return default(T); 
    } 


    public bool Add<T>(string key, T value) 
    { 
     return CacheAdd(key, value); 
    } 
} 

и теперь я пытаюсь проверить его с кодом, который основан офф blog post из @ayende.

var w = new ManualResetEvent(false); 
var threads = new List<Thread>(); 
for (int i = 0; i < Environment.ProcessorCount; i++) 
{ 
    threads.Add(new Thread(() => 
    { 
     w.WaitOne(); 
     DoWork(i); 
    })); 
    threads.Last().Start(); 
} 

w.Set();//release all threads to start at the same time 
foreach (var thread in threads) 
{ 
    thread.Join(); 
} 

Так DoWork вызывает процесс, который содержит одноплодную менеджер кэша и в моем случае это собирается покинуть и аутентификацию из системы и возвращает маркер. Этот токен затем сохраняется с помощью уникального ключа (имя пользователя). Теперь каждый из этих вызовов, в моем случае есть 8 ядер/потоков, имя пользователя такое же, скажем, «BobUser: CacheKey».

Каждый раз, когда я запускаю код, я вижу, что выполняется 8 запросов, так как кеш Get всегда возвращает null.

var token = _cm.Cache.Get<MyToken>(userId); 
if (token != null) return token; 
token = base.Logon(userId, password); 
if (token != null) 
{ 
    _cm.Cache.Add(userId, token); 
} 
return token; 

Действительно ли это из-за взаимодействия 8 потоков Точно в одно и то же время? Если это так, есть ли шаблон для исправления этой проблемы параллелизма?

Спасибо, Стивен

ответ

2

Это происходит потому, что для того, чтобы кэш пнуть в, по меньшей мере, один поток должен был бы полностью закончить проверку подлинности, и добавление в кэш, прежде чем другие потоки даже добраться до .Get() вызовите вверху.

var token = _cm.Cache.Get<MyToken>(userId); // <-------------------------------+ 
if (token != null) return token;   //         | 
token = base.Logon(userId, password);  //         | 
if (token != null)       //         | 
{           //         | 
    _cm.Cache.Add(userId, token); //<-- A thread needs to execute this before | 
            // other threads even execute this ---------- 
} 
return token; 

Вы делаете что-то хуже, блокируя потоки и запуская их примерно в одно и то же время. Если вы хотите исправить это, поместите lock вокруг вашей аутентификации, чтобы только один запрос на проверку подлинности удалялся одновременно.

Пример может выглядеть следующим образом:

private static object AuthenticationLocker = new object(); 
private string GetToken(string userId) 
{ 
    lock (AuthenticationLocker) 
    { 
     var token = _cm.Cache.Get<MyToken>(userId); 
     if (token != null) return token; 
     token = base.Logon(userId, password); 
     if (token != null) 
     { 
      _cm.Cache.Add(userId, token); 
     } 
     return token; 
    } 
} 

Обратите внимание, что если вы начнете использовать замки, вы можете использовать обычный кэш, а не кэш потокобезопасного.

Если вы не хотите использовать блокировки, вам необходимо принять тот факт, что одновременно может быть отправлено несколько запросов. Вы не можете иметь оба варианта

+1

Я усугубил проблему с помощью многопоточного испытательного жгута. Этот код будет жить в хосте ASP.NET/WCF, где он должен «работать» достаточно хорошо. Мое решение во время тестирования состоит в том, чтобы перенести тест перед началом работы с одним запросом, который не имеет никаких утверждений, тем самым полностью продвигая его. –

1

гляньте на участке So how does ConcurrentDictionary do better?

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

+0

Отлично читается, особенно если вы следуете всем гиперссылкам. –

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

  • Нет связанных вопросов^_^