ConfigureAwait(false) 和 IAsyncDisposable 的结构实现
ConfigureAwait(false) and struct implementation of IAsyncDisposable
我已经使用 ActionOnAsyncDispose 结构实现了 IAsyncDisposable,如下所示。我的理解是编译器在async using statement:
的时候是不会装箱的
ActionOnDisposeAsync x = ...;
await using (x) {
...
}
对吗?到目前为止,一切都很好。我的问题是,当我像这样配置 await 时:
ActionOnDisposeAsync x = ...;
await using (x.ConfigureAwait()) {
...
}
x会装箱吗?如果我将 ConfigureAwait 放入方法 Caf():
中会怎么样
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static public ConfiguredAsyncDisposable Caf(this ActionOnDisposeAsync disposable)
=> disposable.ConfigureAwait(false);
ActionOnDisposeAsync x = ...;
await using (x.Caf()) {
...
}
在那种情况下我可以避免装箱吗?我无法找到关于我的 using 变量究竟需要实现什么才能获得 ConfigureAwait 效果的文档。似乎也没有任何 public 构造 ConfiguredAsyncDisposable 的方法。
这是 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();
}
}
...
}
如果编译器能够检测到实际类型(您的结构),则不需要装箱。如果它仅通过接口工作,它将在处理时使用。我用 ILSpy 之类的东西检查你的编译代码,你会看到 dispose 语句是在 class(对于接口也是这种情况)还是在值类型(/struct)上完成的。
我不确定在处理异步时使用结构是否会给你带来很多好处,以及它是否值得付出努力,但你应该在决定之前衡量一下。
是的,struct
一次性用品上的 ConfigureAwait
导致拳击。这是此行为的实验演示:
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'");
...其中 MyDisposableStruct
是这个简单的结构:
readonly struct MyDisposableStruct : IAsyncDisposable
{
public ValueTask DisposeAsync() => default;
}
输出:
Allocated: 24 bytes per 'await using'
为了防止装箱的发生,您必须创建一个自定义的类似 ConfiguredAsyncDisposable
的结构,它是专门为您的结构量身定制的。这是如何完成的:
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);
}
现在 运行 与以前相同的实验,在不对代码进行任何更改的情况下,不会导致分配。输出为:
Allocated: 0 bytes per 'await using'
我已经使用 ActionOnAsyncDispose 结构实现了 IAsyncDisposable,如下所示。我的理解是编译器在async using statement:
的时候是不会装箱的ActionOnDisposeAsync x = ...;
await using (x) {
...
}
对吗?到目前为止,一切都很好。我的问题是,当我像这样配置 await 时:
ActionOnDisposeAsync x = ...;
await using (x.ConfigureAwait()) {
...
}
x会装箱吗?如果我将 ConfigureAwait 放入方法 Caf():
中会怎么样[MethodImpl(MethodImplOptions.AggressiveInlining)]
static public ConfiguredAsyncDisposable Caf(this ActionOnDisposeAsync disposable)
=> disposable.ConfigureAwait(false);
ActionOnDisposeAsync x = ...;
await using (x.Caf()) {
...
}
在那种情况下我可以避免装箱吗?我无法找到关于我的 using 变量究竟需要实现什么才能获得 ConfigureAwait 效果的文档。似乎也没有任何 public 构造 ConfiguredAsyncDisposable 的方法。
这是 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();
}
}
...
}
如果编译器能够检测到实际类型(您的结构),则不需要装箱。如果它仅通过接口工作,它将在处理时使用。我用 ILSpy 之类的东西检查你的编译代码,你会看到 dispose 语句是在 class(对于接口也是这种情况)还是在值类型(/struct)上完成的。
我不确定在处理异步时使用结构是否会给你带来很多好处,以及它是否值得付出努力,但你应该在决定之前衡量一下。
是的,struct
一次性用品上的 ConfigureAwait
导致拳击。这是此行为的实验演示:
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'");
...其中 MyDisposableStruct
是这个简单的结构:
readonly struct MyDisposableStruct : IAsyncDisposable
{
public ValueTask DisposeAsync() => default;
}
输出:
Allocated: 24 bytes per 'await using'
为了防止装箱的发生,您必须创建一个自定义的类似 ConfiguredAsyncDisposable
的结构,它是专门为您的结构量身定制的。这是如何完成的:
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);
}
现在 运行 与以前相同的实验,在不对代码进行任何更改的情况下,不会导致分配。输出为:
Allocated: 0 bytes per 'await using'