ManagedSession: release the reader lock from a pooled state machine

ExitReaderOnCompletion registered the release as a continuation on the
service call's task, so the caller's await was the second continuation
and allocated the task's continuation list (~80 B per request). On .NET 6
and later the call is awaited in a pooled state machine that releases the
lock before its own result completes; net48 keeps the continuation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Roman Ettlinger
2026-10-19 15:04:37 +02:00
co-authored by Claude Opus 5.5
parent 8edf352b93
commit 4528d04676
@@ -28,6 +28,7 @@
* ======================================================================*/ * ======================================================================*/
using System; using System;
using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -96,7 +97,9 @@ namespace Opc.Ua.Client
/// </summary> /// </summary>
public AsyncReaderWriterLock() public AsyncReaderWriterLock()
{ {
#if !NET6_0_OR_GREATER
m_releaseReader = ReleaseReader; m_releaseReader = ReleaseReader;
#endif
} }
/// <summary> /// <summary>
@@ -187,13 +190,16 @@ namespace Opc.Ua.Client
/// task for the same result. /// task for the same result.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The release is the first continuation registered on the operation's /// The lock is free again by the time an awaiter of the returned task
/// task and runs synchronously when it completes, so the lock is normally /// resumes, as with a <c>using</c> block around the awaited operation.
/// free again by the time an awaiter of the returned task resumes, as with /// On .NET 6 and later the returned task awaits the operation in a pooled
/// a <c>using</c> block around the awaited operation. Only if the task /// state machine, the only continuation of the operation, so this
/// completes while the continuation is being registered does the release /// allocates nothing in the steady state; the returned task must be
/// run on the thread pool instead, shortly after. The delegate is cached, /// awaited once, like any value task. Otherwise the release is the first
/// so this allocates nothing beyond the task's continuation list. /// continuation registered on the operation's task and the caller's await
/// the second, which allocates the task's continuation list; only if the
/// task completes while the release is being registered does it run on
/// the thread pool, shortly after.
/// </remarks> /// </remarks>
/// <typeparam name="T">The result of the operation.</typeparam> /// <typeparam name="T">The result of the operation.</typeparam>
public ValueTask<T> ExitReaderOnCompletion<T>(ValueTask<T> operation) public ValueTask<T> ExitReaderOnCompletion<T>(ValueTask<T> operation)
@@ -203,11 +209,34 @@ namespace Opc.Ua.Client
ReleaseReader(); ReleaseReader();
return operation; return operation;
} }
#if NET6_0_OR_GREATER
return ExitReaderAfterAsync(operation);
#else
Task<T> task = operation.AsTask(); Task<T> task = operation.AsTask();
task.ConfigureAwait(false).GetAwaiter().UnsafeOnCompleted(m_releaseReader); task.ConfigureAwait(false).GetAwaiter().UnsafeOnCompleted(m_releaseReader);
return new ValueTask<T>(task); return new ValueTask<T>(task);
#endif
} }
#if NET6_0_OR_GREATER
/// <summary>
/// Awaits <paramref name="operation"/> and releases the reader lock.
/// </summary>
/// <typeparam name="T">The result of the operation.</typeparam>
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
private async ValueTask<T> ExitReaderAfterAsync<T>(ValueTask<T> operation)
{
try
{
return await operation.ConfigureAwait(false);
}
finally
{
ReleaseReader();
}
}
#endif
/// <summary> /// <summary>
/// Asynchronously acquires the writer lock. Excludes both /// Asynchronously acquires the writer lock. Excludes both
/// other writers and all readers for the duration the lock /// other writers and all readers for the duration the lock
@@ -390,7 +419,9 @@ namespace Opc.Ua.Client
} }
} }
#if !NET6_0_OR_GREATER
private readonly Action m_releaseReader; private readonly Action m_releaseReader;
#endif
private bool m_isDisposed; private bool m_isDisposed;
private readonly SemaphoreSlim m_writer = new(1, 1); private readonly SemaphoreSlim m_writer = new(1, 1);
private readonly Lock m_state = new(); private readonly Lock m_state = new();