【问题标题】:Implementing ConcurrentDictionary实现并发字典
【发布时间】:2018-11-21 15:55:19
【问题描述】:

我正在尝试为 API 创建自己的缓存实现。这是我第一次使用 ConcurrentDictionary,我不知道我是否正确使用它。在测试中,出现了一些错误,到目前为止我还无法再次重现它。也许一些并发专业人士/ConcurrentDictionary 可以查看代码并找出可能出错的地方。谢谢!

private static readonly ConcurrentDictionary<string, ThrottleInfo> CacheList = new ConcurrentDictionary<string, ThrottleInfo>();

public override void OnActionExecuting(HttpActionContext actionExecutingContext)
{
    if (CacheList.TryGetValue(userIdentifier, out var throttleInfo))
    {
        if (DateTime.Now >= throttleInfo.ExpiresOn)
        {
            if (CacheList.TryRemove(userIdentifier, out _))
            {
                //TODO:
            }
        }
        else
        {
            if (throttleInfo.RequestCount >= defaultMaxRequest)
            {
                actionExecutingContext.Response = ResponseMessageExtension.TooManyRequestHttpResponseMessage();
            }
            else
            {
                throttleInfo.Increment();
            }
        }

    }
    else
    {
        if (CacheList.TryAdd(userIdentifier, new ThrottleInfo(Seconds)))
        {
            //TODO:
        }
    }
}

public class ThrottleInfo
{
    private int _requestCount;

    public int RequestCount => _requestCount;

    public ThrottleInfo(int addSeconds)
    {
        Interlocked.Increment(ref _requestCount);
        ExpiresOn = ExpiresOn.AddSeconds(addSeconds);
    }

    public void Increment()
    {
        // this is about as thread safe as you can get.
        // From MSDN: Increments a specified variable and stores the result, as an atomic operation.
        Interlocked.Increment(ref _requestCount);

        // you can return the result of Increment if you want the new value,
        //but DO NOT set the counter to the result :[i.e. counter = Interlocked.Increment(ref counter);] This will break the atomicity.
    }

    public DateTime ExpiresOn { get; } = DateTime.Now;
}

【问题讨论】:

  • 你遇到了什么错误?
  • 这就是问题所在,当它发生时,我没有做笔记,现在我无法重现它。我试过了,但我做不到。我认为这是一个并发问题。在某些时候,2 个或更多线程想要对同一个对象做某事并破坏代码。几行代码,也许是一些专家,可以可视化并发现一些错误。这是我第一次使用 ConcurrentDictionary
  • ConcurrentDictionary 类只适用于非常琐碎的缓存场景。对于更高级的东西(如过期策略),有专门的类可用。像System.Runtime.Caching.MemoryCache(带有string 键)和较新的Microsoft.Extensions.Caching.Memory.MemoryCache(带有object 键)。后者提供更多优先级选项。我强烈建议使用其中之一。

标签: c# multithreading thread-safety concurrentdictionary


【解决方案1】:

如果我了解您在 ExpiresOn 已通过的情况下尝试做什么,请删除条目,否则更新它或添加(如果不存在)。 您当然可以利用 AddOrUpdateMethod 来简化一些代码。 看看这里有一些很好的例子:https://docs.microsoft.com/en-us/dotnet/standard/collections/thread-safe/how-to-add-and-remove-items 希望这会有所帮助。

【讨论】:

    【解决方案2】:

    ConcurrentDictionary 仅在以下情况下才足够作为线程安全容器: (1) 需要保护的整个状态是其内部状态(它包含的键和值),并且仅当 (2) 此状态可以使用它提供的专用 API(GetOrAddAddOrUpdate)进行原子突变。在您的情况下,不满足第二个要求,因为您需要 remove keys conditionally 取决于它们的值的状态,而 ConcurrentDictionary 类不支持这种情况。

    所以你当前的缓存实现不是线程安全的。偶尔抛出异常的事实是巧合。如果它完全防抛出,它仍然是非线程安全的,因为它不会完全防错,这意味着它可能偶尔(或永久)转换到与其规范不兼容的状态(例如返回过期值)。

    关于ThrottleInfo 类,它存在一个可见性错误,如果您在一台机器上广泛测试该类,它可能无法观察到,然后当您将应用程序部署到具有不同 CPU 架构的另一台机器上时突然出现。非volatile private int _requestCount 字段通过公共属性RequestCount 公开,因此不能保证(基于C# 规范)所有线程都会看到其最新值。您可以阅读 Igor Ostrovsky 撰写的 this article 关于内存模型的特殊性,这可能会让您(像我一样)相信在多线程代码中使用无锁技术(在这种情况下使用 Interlocked 类)比它更麻烦值得。如果您阅读并喜欢它,还有这篇文章的part 2

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多