【问题标题】:Interlocked.Increment on dictionary valueInterlocked.Increment 字典值
【发布时间】:2015-11-18 09:15:00
【问题描述】:

我正在尝试使用字典来记录 Web 服务上每个 API 路径的当前请求计数,并增加和减少当前计数我认为使用Interlocked.Increment() 的好方法是因为它增加了计数和同时读取计数。

但是下面的代码给出了一个错误,说 ref 参数没有被归类为变量,我猜这是因为 dict[key] 不是变量?

var dict = new Dictionary<string, int>();
dict.Add("a", 1);
int i = Interlocked.Increment(ref dict["a"]);

我知道Interlocked.Increment() 不能应用于属性,但没想到通过键访问字典会遇到同样的问题。

最好的方法是什么?

编辑:这里有一些关于此的更多细节。

我正在尝试编写一些代码来限制 Web 服务的每个 API 路径上的 API 调用次数,所以我有两个字典,一个策略字典指定每个 API 路径上允许多少个并发调用者和一个第二个计数器字典,用于记录每个 API 路径上当前有多少调用者处于活动状态。

在 Web 服务执行任何传入请求之前,它会检查上述两个字典以决定该请求应该继续还是直接返回 HTTP 429(请求过多)响应。

这是代码的摘录,它首先检查是否有匹配的策略,如果有,然后检查是否超出了允许的最大请求数。

public override async Task Invoke(IOwinContext context)
{
    var url = context.Request.Path.ToString();
    var policy = _policies.FirstOrDefault(x => x.EndpointPath == url);

    if (policy != null)
    {
        try
        {
            if (Interlocked.Increment(ref _currentRequests[policy]) > policy.MaxConcurrentConnection)
            {
                context.Response.StatusCode = 429;
                var message = string.Format(
                    "Max API concurrent calls quota exceeded, please try again later. Maximum admitted: {0}",
                    policy.MaxConcurrentConnection);
                context.Response.Write(message);
                context.Response.ReasonPhrase = "Too Many Requests";
            }
            else
            {
                await Next.Invoke(context);
            }
        }
        finally
        {
            Interlocked.Decrement(ref _currentRequests[policy]);
        }
    }
    else
    {
        await Next.Invoke(context);
    }
}

【问题讨论】:

  • 你想做什么?即使此代码有效,它也不能保证存储值的线程安全修改。它只会保证返回的副本的线程安全。也许您正在寻找 ConcurrentDictionary 或不可变类之一?
  • @PanagiotisKanavos ,为什么不能保证字典中值的线程安全修改..我希望 Interlocked.Increment (参考某些东西)将确保不同的线程可以修改和读取的值安全的东西。我将编辑问题以提供更多详细信息。谢谢。
  • 尝试使用lock语句而不是Interlocked
  • 简单地说。字典是通过属性获取完成的,因此您不能对其值进行互锁的操作。但是,您可以对数组单元执行互锁操作 - 它们是直接访问的固定内存位置。因此,您将设置一个大数组,然后您的字典将返回该数组的索引,而不是返回计数本身,然后将计数存储在数组中。然后你可以对数组单元进行互锁操作。当然,您可以将其封装在一个类中,这样使用该类就与使用字典一样。
  • 另一种方法是字典中的每个项目都是一个“计数”对象。您调用这些对象上的方法来递增、递减和获取当前值。在内部,这些对象使用 Interlocked 来递增和递减它们的私有 int,使用属性访问器以只读方式访问。

标签: c# .net multithreading web-services


【解决方案1】:

在字典中存储一个可变堆对象:

ConcurrentDictionary<..., StrongBox<int>> dict = ...;
Interlocked.Increment(ref dict[...].Value);

StrongBox.Value 是一个可变字段。

【讨论】:

  • @user8549339 请比“错误”更具体。我将答案更改为使用 ConcurrentDictionary 以便您可以安全地添加它。这确实是一个错误。
  • 这实际上与我自己的代码有关。它确实有效 :) 虽然对于我当前的设置,但我不需要 ConcurrentDictionary。
【解决方案2】:

实际上要容易得多。特别是如果您不确定是否创建了密钥。

如果存在,下面的值将加一,如果不存在,则使用默认值 1 创建它。 Concurrents 命名空间包含构建线程安全对象所需的几乎所有内容。我通常不喜欢使用锁,因为它会序列化对对象的访问(如果我们要串行执行,为什么还要执行多线程?)

ConcurrentDictionary<string, int> dataStore = new ConcurrentDictionary<string, int>();

public int Increment(string key)
{
    return dataStore.AddOrUpdate(key, 1, (k, v) => Interlocked.Increment(ref v));
}

【讨论】:

  • updateValueFactory 委托中使用Interlocked.Increment 是多余的。 (k, v) =&gt; v + 1 可以正常工作。 v 变量是本地的,不能被其他线程并发访问。 AddOrUpdate 方法通过丢弃返回值并重新调用委托来伪造原子性,以防另一个线程更快地更新特定键。
  • @TheodorZoulias,没错,并发字典实现中有一个锁。
  • 这对很多不太了解 ConcurrentDictionary 的人来说是一种误导,因此我建议将其删除。
  • 这是必需的,因为 updateValueFactory 上没有锁定。参考docs.microsoft.com/en-us/dotnet/api/…However, the updateValueFactory delegate is called outside the locks to avoid the problems that can arise from executing unknown code under a lock
【解决方案3】:

您使用Interlocked 的主要原因是性能。如果您没有遇到性能问题,那么您的代码将可以被更多人理解,并且更易于编写和阅读,如果您只需使用 lock

如果你绝对必须使用Interlocked,那么你就不能按照你尝试的方式使用字典。 Interlocked 操作是原子的,通常在 CPU 级别,它们需要在内存中的固定位置进行操作。字典上的属性访问器不提供这个。

如果您仍然想使用字典,有两种方法可以想到:

将您的计数存储在一个数组中

您可以将计数值存储在一个数组中。每个单元格在内存中是固定的,因此可以被Interlocked 使用。您的字典,而不是存储计数,会将索引存储在存储计数的这个数组中。当然,您可以将这些全部写入一个类中,以便隐藏这些实现细节。

在字典中存储“计数对象”

字典中的每一项都是保存计数的类的一个实例。在类内部,将有一个private int 可供Interlocked 使用。你的类将提供IncrementDecrement 方法,以及一个只读的Count 属性,以允许以类似的方式使用它。

编辑

使用信号量

实际上,您可以研究的另一件事是使用Semaphore。他们几乎是designed for this。通过让 Dictionary 中的每个单元格为 Semaphore 而不是计数,您可以以线程安全的方式实现非常相似的事情。你会做dictionary[key].WaitOne(0),如果成功则返回true,否则返回false。如果它确实返回真,那么信号量的计数已经增加,你只需稍后再次调用Dictionary[hey].Release()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-06-27
    • 2010-11-05
    • 1970-01-01
    • 2019-04-14
    • 1970-01-01
    • 2015-10-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多