Skip to content

Commit a2f7d06

Browse files
JamesNKgfoidl
andauthored
Avoid using ConcurrentDictionary for channels with few methods (#2597)
Co-authored-by: Günther Foidl <[email protected]>
1 parent c9d2671 commit a2f7d06

File tree

4 files changed

+177
-5
lines changed

4 files changed

+177
-5
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515

1616
build:
1717
name: Basic Tests
18-
runs-on: ubuntu-latest
18+
runs-on: ubuntu-22.04
1919
steps:
2020

2121
- name: Check out code
@@ -36,7 +36,7 @@ jobs:
3636

3737
grpc_web:
3838
name: gRPC-Web Tests
39-
runs-on: ubuntu-latest
39+
runs-on: ubuntu-22.04
4040
steps:
4141

4242
- name: Check out code

src/Grpc.Net.Client/GrpcChannel.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
#endregion
1818

19-
using System.Collections.Concurrent;
2019
using System.Diagnostics;
2120
using Grpc.Core;
2221
#if SUPPORT_LOAD_BALANCING
@@ -51,7 +50,7 @@ public sealed partial class GrpcChannel : ChannelBase, IDisposable
5150
internal const long DefaultMaxRetryBufferPerCallSize = 1024 * 1024; // 1 MB
5251

5352
private readonly object _lock;
54-
private readonly ConcurrentDictionary<IMethod, GrpcMethodInfo> _methodInfoCache;
53+
private readonly ThreadSafeLookup<IMethod, GrpcMethodInfo> _methodInfoCache;
5554
private readonly Func<IMethod, GrpcMethodInfo> _createMethodInfoFunc;
5655
private readonly Dictionary<MethodKey, MethodConfig>? _serviceConfigMethods;
5756
private readonly bool _isSecure;
@@ -109,7 +108,7 @@ public sealed partial class GrpcChannel : ChannelBase, IDisposable
109108
internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(address.Authority)
110109
{
111110
_lock = new object();
112-
_methodInfoCache = new ConcurrentDictionary<IMethod, GrpcMethodInfo>();
111+
_methodInfoCache = new ThreadSafeLookup<IMethod, GrpcMethodInfo>();
113112

114113
// Dispose the HTTP client/handler if...
115114
// 1. No client/handler was specified and so the channel created the client itself
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
using System.Collections.Concurrent;
20+
21+
internal sealed class ThreadSafeLookup<TKey, TValue> where TKey : notnull
22+
{
23+
// Avoid allocating ConcurrentDictionary until the threshold is reached.
24+
// Looking up a key in an array is as fast as a dictionary for small collections and uses much less memory.
25+
internal const int Threshold = 10;
26+
27+
private KeyValuePair<TKey, TValue>[] _array = Array.Empty<KeyValuePair<TKey, TValue>>();
28+
private ConcurrentDictionary<TKey, TValue>? _dictionary;
29+
30+
/// <summary>
31+
/// Gets the value for the key if it exists. If the key does not exist then the value is created using the valueFactory.
32+
/// The value is created outside of a lock and there is no guarentee which value will be stored or returned.
33+
/// </summary>
34+
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
35+
{
36+
if (_dictionary != null)
37+
{
38+
return _dictionary.GetOrAdd(key, valueFactory);
39+
}
40+
41+
if (TryGetValue(_array, key, out var value))
42+
{
43+
return value;
44+
}
45+
46+
var newValue = valueFactory(key);
47+
48+
lock (this)
49+
{
50+
if (_dictionary != null)
51+
{
52+
_dictionary.TryAdd(key, newValue);
53+
}
54+
else
55+
{
56+
// Double check inside lock if the key was added to the array by another thread.
57+
if (TryGetValue(_array, key, out value))
58+
{
59+
return value;
60+
}
61+
62+
if (_array.Length > Threshold - 1)
63+
{
64+
// Array length exceeds threshold so switch to dictionary.
65+
var newDict = new ConcurrentDictionary<TKey, TValue>();
66+
foreach (var kvp in _array)
67+
{
68+
newDict.TryAdd(kvp.Key, kvp.Value);
69+
}
70+
newDict.TryAdd(key, newValue);
71+
72+
_dictionary = newDict;
73+
_array = Array.Empty<KeyValuePair<TKey, TValue>>();
74+
}
75+
else
76+
{
77+
// Add new value by creating a new array with old plus new value.
78+
var newArray = new KeyValuePair<TKey, TValue>[_array.Length + 1];
79+
Array.Copy(_array, newArray, _array.Length);
80+
newArray[newArray.Length - 1] = new KeyValuePair<TKey, TValue>(key, newValue);
81+
82+
_array = newArray;
83+
}
84+
}
85+
}
86+
87+
return newValue;
88+
}
89+
90+
private static bool TryGetValue(KeyValuePair<TKey, TValue>[] array, TKey key, out TValue value)
91+
{
92+
foreach (var kvp in array)
93+
{
94+
if (EqualityComparer<TKey>.Default.Equals(kvp.Key, key))
95+
{
96+
value = kvp.Value;
97+
return true;
98+
}
99+
}
100+
101+
value = default!;
102+
return false;
103+
}
104+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
namespace Grpc.Net.Client.Tests;
20+
21+
[TestFixture]
22+
public class ThreadSafeLookupTests
23+
{
24+
[Test]
25+
public void GetOrAdd_ReturnsCorrectValueForNewKey()
26+
{
27+
var lookup = new ThreadSafeLookup<int, string>();
28+
var result = lookup.GetOrAdd(1, k => "Value-1");
29+
30+
Assert.AreEqual("Value-1", result);
31+
}
32+
33+
[Test]
34+
public void GetOrAdd_ReturnsExistingValueForExistingKey()
35+
{
36+
var lookup = new ThreadSafeLookup<int, string>();
37+
lookup.GetOrAdd(1, k => "InitialValue");
38+
var result = lookup.GetOrAdd(1, k => "NewValue");
39+
40+
Assert.AreEqual("InitialValue", result);
41+
}
42+
43+
[Test]
44+
public void GetOrAdd_SwitchesToDictionaryAfterThreshold()
45+
{
46+
var addCount = (ThreadSafeLookup<int, string>.Threshold * 2);
47+
var lookup = new ThreadSafeLookup<int, string>();
48+
49+
for (var i = 0; i <= addCount; i++)
50+
{
51+
lookup.GetOrAdd(i, k => $"Value-{k}");
52+
}
53+
54+
var result = lookup.GetOrAdd(addCount, k => $"NewValue-{addCount}");
55+
56+
Assert.AreEqual($"Value-{addCount}", result);
57+
}
58+
59+
[Test]
60+
public void GetOrAdd_HandlesConcurrentAccess()
61+
{
62+
var lookup = new ThreadSafeLookup<int, string>();
63+
Parallel.For(0, 1000, i =>
64+
{
65+
var value = lookup.GetOrAdd(i, k => $"Value-{k}");
66+
Assert.AreEqual($"Value-{i}", value);
67+
});
68+
}
69+
}

0 commit comments

Comments
 (0)