Added notes to the docs about thread safety of implementations of IAsyncState, IAsyncContext<T> and IAsyncLocalContext<T> (#4881)

This commit is contained in:
Martin Obrátil 2024-01-16 20:36:11 +01:00 коммит произвёл GitHub
Родитель d58517b455 6ed0e2c14c
Коммит f75874b0ff
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
12 изменённых файлов: 78 добавлений и 67 удалений

Просмотреть файл

@ -16,6 +16,7 @@ public static class AsyncStateHttpContextExtensions
/// <summary>
/// Adds default implementations for <see cref="IAsyncState"/>, <see cref="IAsyncContext{T}"/>, and <see cref="IAsyncLocalContext{T}"/> services,
/// scoped to the lifetime of <see cref="AspNetCore.Http.HttpContext"/> instances.
/// Please note that implementations of these interfaces are not thread safe.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
/// <returns>The value of <paramref name="services"/>.</returns>

Просмотреть файл

@ -4,6 +4,9 @@ This provides the ability to store and retrieve state objects that flow with the
The lifetime of the shared data is controlled automatically and will be the same as of `HttpContext`.
> [!NOTE]
> Please note, the implementation of `IAsyncContext<T>` provided by this library is not thread-safe.
## Install the package
From the command-line:

Просмотреть файл

@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Shared.Diagnostics;
@ -12,7 +13,7 @@ namespace Microsoft.Extensions.AsyncState;
internal sealed class AsyncState : IAsyncState
{
private static readonly AsyncLocal<AsyncStateHolder> _asyncContextCurrent = new();
private static readonly ObjectPool<Features> _featuresPool = PoolFactory.CreatePool(new FeaturesPooledPolicy());
private static readonly ObjectPool<List<object?>> _featuresPool = PoolFactory.CreatePool(new FeaturesPooledPolicy());
private int _contextCount;
public void Initialize()
@ -21,12 +22,12 @@ internal sealed class AsyncState : IAsyncState
// Use an object indirection to hold the AsyncContext in the AsyncLocal,
// so it can be cleared in all ExecutionContexts when its cleared.
var asyncStateHolder = new AsyncStateHolder
var features = new AsyncStateHolder
{
Features = _featuresPool.Get()
};
_asyncContextCurrent.Value = asyncStateHolder;
_asyncContextCurrent.Value = features;
}
public void Reset()
@ -59,7 +60,9 @@ internal sealed class AsyncState : IAsyncState
return false;
}
value = _asyncContextCurrent.Value.Features.Get(token.Index);
EnsureCount(_asyncContextCurrent.Value.Features, token.Index + 1);
value = _asyncContextCurrent.Value.Features[token.Index];
return true;
}
@ -83,14 +86,28 @@ internal sealed class AsyncState : IAsyncState
Throw.InvalidOperationException("Context is not initialized");
}
_asyncContextCurrent.Value.Features.Set(token.Index, value);
EnsureCount(_asyncContextCurrent.Value.Features, token.Index + 1);
_asyncContextCurrent.Value.Features[token.Index] = value;
}
internal static void EnsureCount(List<object?> features, int count)
{
#if NET6_0_OR_GREATER
features.EnsureCapacity(count);
#endif
var difference = count - features.Count;
for (int i = 0; i < difference; i++)
{
features.Add(null);
}
}
internal int ContextCount => Volatile.Read(ref _contextCount);
private sealed class AsyncStateHolder
{
public Features? Features { get; set; }
public List<object?>? Features { get; set; }
}
}

Просмотреть файл

@ -15,6 +15,7 @@ public static class AsyncStateExtensions
{
/// <summary>
/// Adds default implementations for <see cref="IAsyncState"/>, <see cref="IAsyncContext{T}"/>, and <see cref="IAsyncLocalContext{T}"/> services.
/// Please note that implementations of these interfaces are not thread safe.
/// </summary>
/// <param name="services">The dependency injection container to add the implementations to.</param>
/// <returns>The value of <paramref name="services"/>.</returns>

Просмотреть файл

@ -1,45 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
namespace Microsoft.Extensions.AsyncState;
internal sealed class Features
{
private readonly List<object?> _items = [];
public object? Get(int index)
{
return _items.Count <= index ? null : _items[index];
}
public void Set(int index, object? value)
{
if (_items.Count <= index)
{
lock (_items)
{
var count = index + 1;
#if NET6_0_OR_GREATER
_items.EnsureCapacity(count);
#endif
var difference = count - _items.Count;
for (int i = 0; i < difference; i++)
{
_items.Add(null);
}
}
}
_items[index] = value;
}
public void Clear()
{
_items.Clear();
}
}

Просмотреть файл

@ -1,22 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.Extensions.AsyncState;
internal sealed class FeaturesPooledPolicy : IPooledObjectPolicy<Features>
internal sealed class FeaturesPooledPolicy : IPooledObjectPolicy<List<object?>>
{
/// <inheritdoc/>
public Features Create()
public List<object?> Create()
{
return new Features();
return [];
}
/// <inheritdoc/>
public bool Return(Features obj)
public bool Return(List<object?> obj)
{
obj.Clear();
for (int i = 0; i < obj.Count; i++)
{
obj[i] = null;
}
return true;
}
}

Просмотреть файл

@ -8,6 +8,7 @@ namespace Microsoft.Extensions.AsyncState;
/// <summary>
/// Provides access to the current async context.
/// Some implementations of this interface may not be thread safe.
/// </summary>
/// <typeparam name="T">The type of the asynchronous state.</typeparam>
[SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Getter and setter throw exceptions.")]

Просмотреть файл

@ -7,6 +7,7 @@ namespace Microsoft.Extensions.AsyncState;
/// <summary>
/// Provides access to the current async context stored outside of the HTTP pipeline.
/// Some implementations of this interface may not be thread safe.
/// </summary>
/// <typeparam name="T">The type of the asynchronous state.</typeparam>
/// <remarks>This type is intended for internal use. Use <see cref="IAsyncContext{T}"/> instead.</remarks>

Просмотреть файл

@ -9,6 +9,7 @@ namespace Microsoft.Extensions.AsyncState;
/// <summary>
/// Encapsulates all information within the asynchronous flow in an <see cref="AsyncLocal{T}"/> variable.
/// Some implementations of this interface may not be thread safe.
/// </summary>
[SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Getter and setter throw exceptions.")]
public interface IAsyncState

Просмотреть файл

@ -1,12 +1,14 @@
# Microsoft.Extensions.AsyncState
This provides the ability to store and retrieve objects that flow with the current asynchronous context.
It has a few advantages over using the [`AsyncLocal<T>`](https://learn.microsoft.com/dotnet/api/system.threading.asynclocal-1) class directly:
- By abstracting the way the ambient data is stored we can use more optimized implementations, for instance when using ASP.NET Core, without exposing these components.
- Improves the performance by minimizing the number of `AsyncLocal<T>` instances required when multiple objects are shared.
- Provides a way to manage the lifetime of the ambient data objects.
> [!NOTE]
> Please note, the implementations of `IAsyncState` and `IAsyncContext<T>` are not thread-safe.
## Install the package
From the command-line:

Просмотреть файл

@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
@ -204,4 +205,28 @@ public class AsyncStateTests
Assert.Equal(3, asyncState.ContextCount);
}
[Fact]
public void EnsureCount_IncreasesCountCorrectly()
{
var l = new List<object?>();
AsyncState.EnsureCount(l, 5);
Assert.Equal(5, l.Count);
}
[Fact]
public void EnsureCount_WhenCountLessThanExpected()
{
var l = new List<object?>(new object?[5]);
AsyncState.EnsureCount(l, 2);
Assert.Equal(5, l.Count);
}
[Fact]
public void EnsureCount_WhenCountEqualWithExpected()
{
var l = new List<object?>(new object?[5]);
AsyncState.EnsureCount(l, 5);
Assert.Equal(5, l.Count);
}
}

Просмотреть файл

@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using Xunit;
namespace Microsoft.Extensions.AsyncState.Test;
@ -12,7 +13,7 @@ public class FeaturesPooledPolicyTests
{
var policy = new FeaturesPooledPolicy();
Assert.True(policy.Return(new Features()));
Assert.True(policy.Return([]));
}
[Fact]
@ -20,14 +21,12 @@ public class FeaturesPooledPolicyTests
{
var policy = new FeaturesPooledPolicy();
var features = policy.Create();
features.Set(0, string.Empty);
features.Set(1, Array.Empty<int>());
features.Set(2, new object());
var list = policy.Create();
list.Add(string.Empty);
list.Add(Array.Empty<int>());
list.Add(new object());
Assert.True(policy.Return(features));
Assert.Null(features.Get(0));
Assert.Null(features.Get(1));
Assert.Null(features.Get(2));
Assert.True(policy.Return(list));
Assert.All(list, el => Assert.Null(el));
}
}