3

I have implemented IAsyncDisposable with an ActionOnAsyncDispose struct as shown below. My understanding is that the compiler will not box it when it is in an async using statement:

ActionOnDisposeAsync x = ...;
await using (x) {
     ...
}

Correct? So far so good. My question is this, when I configure await on it like so:

ActionOnDisposeAsync x = ...;
await using (x.ConfigureAwait()) {
     ...
}

will x be boxed? What about if I put the ConfigureAwait in a method, Caf():

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static public ConfiguredAsyncDisposable Caf(this ActionOnDisposeAsync disposable)
    => disposable.ConfigureAwait(false);

ActionOnDisposeAsync x = ...;
await using (x.Caf()) {
     ...
}

Can I avoid boxing in that case? I was not able to find documentation on what exactly my using variable needs to implement in order to have the effect of ConfigureAwait. There doesn't seem to be any public way of constructing a ConfiguredAsyncDisposable either.

Here is ActionOnDisposeAsync:

public readonly struct ActionOnDisposeAsync : IAsyncDisposable, IEquatable<ActionOnDisposeAsync>
{
    public ActionOnDisposeAsync(Func<Task> actionAsync)
    {
        this.ActionAsync = actionAsync;
    }
    public ActionOnDisposeAsync( Action actionSync)
    {
        this.ActionAsync = () => { actionSync(); return Task.CompletedTask; };
    }
    private Func<Task> ActionAsync { get; }

    public async ValueTask DisposeAsync()
    {
        if (this.ActionAsync != null) {
            await this.ActionAsync();
        }
    }

    ...
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
sjb-sjb
  • 1,112
  • 6
  • 14
  • You can only avoid boxing when the compiler can statically verify the type of the object. `ConfiguredAsyncDisposable` is statically known to be a struct so that should not be a problem. This does not apply when the declared type of a variable is `IAsyncDisposable`, because then the runtime type is not statically known – Charlieface Apr 03 '22 at 19:29

2 Answers2

4

Yes, the ConfigureAwait on struct disposables causes boxing. Here is an experimental demonstration of this behavior:

MyDisposableStruct value = new();
const int loops = 1000;
var mem0 = GC.GetTotalAllocatedBytes(true);
for (int i = 0; i < loops; i++)
{
    await using (value.ConfigureAwait(false)) { }
}
var mem1 = GC.GetTotalAllocatedBytes(true);
Console.WriteLine($"Allocated: {(mem1 - mem0) / loops:#,0} bytes per 'await using'");

...where MyDisposableStruct is this simple struct:

readonly struct MyDisposableStruct : IAsyncDisposable
{
    public ValueTask DisposeAsync() => default;
}

Output:

Allocated: 24 bytes per 'await using'

Live demo.

To prevent the boxing from happening you will have to create a custom ConfiguredAsyncDisposable-like struct, that is tailored specifically for your struct. Here is how it can be done:

readonly struct MyConfiguredAsyncDisposable
{
    private readonly MyDisposableStruct _parent;
    private readonly bool _continueOnCapturedContext;

    public MyConfiguredAsyncDisposable(MyDisposableStruct parent,
        bool continueOnCapturedContext)
    {
        _parent = parent;
        _continueOnCapturedContext = continueOnCapturedContext;
    }

    public ConfiguredValueTaskAwaitable DisposeAsync()
        => _parent.DisposeAsync().ConfigureAwait(_continueOnCapturedContext);
}

static MyConfiguredAsyncDisposable ConfigureAwait(
    this MyDisposableStruct source, bool continueOnCapturedContext)
{
    return new MyConfiguredAsyncDisposable(source, continueOnCapturedContext);
}

Now running the same experiment as before, without making any change in the code whatsoever, does not cause allocations. The output is:

Allocated: 0 bytes per 'await using'

Live demo.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
0

If the compiler is able to detect the actual type (your struct) it does not require boxing. If it only works via the interface, it will when disposing. My checking your compiled code with something like ILSpy you will see if the dispose statement is done on a class (also the case for interfaces), or on a value type (/struct).

I'm not sure if using a struct will gain you much when disposing async, and if it is worth the effort, but you should measure that before deciding.

Corniel Nobel
  • 421
  • 4
  • 12
  • To restate my question, say I want to implement my own struct that replaces ConfiguredAsyncDisposable. What methods does my struct need to implement in order to be recognized by the complier in an await using statement? – sjb-sjb Apr 06 '22 at 03:26
  • In your example: the compiler will know, as it disposing your struct, an not just any I(Async)Disopasable that is not known runtime. The same is true for things like IEnumerators, although a totally different topic. – Corniel Nobel Apr 07 '22 at 20:19