Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/Grpc.Net.Client/GrpcChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

#endregion

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

private readonly object _lock;
private readonly ConcurrentDictionary<IMethod, GrpcMethodInfo> _methodInfoCache;
private readonly ThreadSafeLookup<IMethod, GrpcMethodInfo> _methodInfoCache;
private readonly Func<IMethod, GrpcMethodInfo> _createMethodInfoFunc;
private readonly Dictionary<MethodKey, MethodConfig>? _serviceConfigMethods;
private readonly bool _isSecure;
Expand Down Expand Up @@ -109,7 +108,7 @@ public sealed partial class GrpcChannel : ChannelBase, IDisposable
internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(address.Authority)
{
_lock = new object();
_methodInfoCache = new ConcurrentDictionary<IMethod, GrpcMethodInfo>();
_methodInfoCache = new ThreadSafeLookup<IMethod, GrpcMethodInfo>();

// Dispose the HTTP client/handler if...
// 1. No client/handler was specified and so the channel created the client itself
Expand Down
84 changes: 84 additions & 0 deletions src/Grpc.Net.Client/Internal/ThreadSafeLookup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using System.Collections.Concurrent;

internal sealed class ThreadSafeLookup<TKey, TValue> where TKey : notnull
{
// Avoid allocating ConcurrentDictionary until the threshold is reached.
// Looking up a key in an array is as fast as a dictionary for small collections and uses much less memory.
internal const int Threshold = 10;

private KeyValuePair<TKey, TValue>[] _array = Array.Empty<KeyValuePair<TKey, TValue>>();
private ConcurrentDictionary<TKey, TValue>? _dictionary;

public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
if (_dictionary != null)
{
return _dictionary.GetOrAdd(key, valueFactory);
}

var snapshot = _array;
foreach (var kvp in snapshot)
{
if (EqualityComparer<TKey>.Default.Equals(kvp.Key, key))
{
return kvp.Value;
}
}

var newValue = valueFactory(key);

if (snapshot.Length + 1 > Threshold)
{
// Lock here to ensure that only one thread will create the initial dictionary.
lock (this)
{
if (_dictionary != null)
{
_dictionary.TryAdd(key, newValue);
}
else
{
var newDict = new ConcurrentDictionary<TKey, TValue>();
foreach (var kvp in snapshot)
{
newDict.TryAdd(kvp.Key, kvp.Value);
}
newDict.TryAdd(key, newValue);

_dictionary = newDict;
_array = Array.Empty<KeyValuePair<TKey, TValue>>();
}
}
}
else
{
// Add new value by creating a new array with old plus new value.
// This allows for lookups without locks and is more memory efficient than a dictionary.
var newArray = new KeyValuePair<TKey, TValue>[snapshot.Length + 1];
Array.Copy(snapshot, newArray, snapshot.Length);
newArray[newArray.Length - 1] = new KeyValuePair<TKey, TValue>(key, newValue);

_array = newArray;
}

return newValue;
}
}
69 changes: 69 additions & 0 deletions test/Grpc.Net.Client.Tests/ThreadSafeLookupTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

namespace Grpc.Net.Client.Tests;

[TestFixture]
public class ThreadSafeLookupTests
{
[Test]
public void GetOrAdd_ReturnsCorrectValueForNewKey()
{
var lookup = new ThreadSafeLookup<int, string>();
var result = lookup.GetOrAdd(1, k => "Value-1");

Assert.AreEqual("Value-1", result);
}

[Test]
public void GetOrAdd_ReturnsExistingValueForExistingKey()
{
var lookup = new ThreadSafeLookup<int, string>();
lookup.GetOrAdd(1, k => "InitialValue");
var result = lookup.GetOrAdd(1, k => "NewValue");

Assert.AreEqual("InitialValue", result);
}

[Test]
public void GetOrAdd_SwitchesToDictionaryAfterThreshold()
{
var addCount = (ThreadSafeLookup<int, string>.Threshold * 2);
var lookup = new ThreadSafeLookup<int, string>();

for (var i = 0; i <= addCount; i++)
{
lookup.GetOrAdd(i, k => $"Value-{k}");
}

var result = lookup.GetOrAdd(addCount, k => $"NewValue-{addCount}");

Assert.AreEqual($"Value-{addCount}", result);
}

[Test]
public void GetOrAdd_HandlesConcurrentAccess()
{
var lookup = new ThreadSafeLookup<int, string>();
Parallel.For(0, 1000, i =>
{
var value = lookup.GetOrAdd(i, k => $"Value-{k}");
Assert.AreEqual($"Value-{i}", value);
});
}
}
Loading