C#のawait usingとは?IAsyncDisposableの使い方とusingとの違いをわかりやすく解説
はじめに
C#で非同期処理を書いていると、asyncやawaitだけでなく、await usingという構文を見かけることがあります。
C#await using var resource = new AsyncResource();
一見すると通常のusingにawaitが付いただけのように見えますが、await usingは単に「非同期処理の中でusingを使う」ための構文ではありません。
await usingは、リソースの破棄処理を非同期で待機するための構文です。
通常のusingでは、スコープを抜けるとDispose()が呼び出されます。一方、await usingでは、スコープを抜けるとDisposeAsync()が呼び出され、その完了をawaitします。
つまり、結論からいうと次のような違いがあります。
C#using // Dispose() を呼び出す
await using // DisposeAsync() を呼び出して await する
この記事では、C#のawait usingとは何か、IAsyncDisposableの使い方、通常のusingとの違い、実務での使いどころをわかりやすく解説します。
1. C#のawait usingとは?
1-1. await usingは非同期でリソースを破棄するための構文
await usingは、C#において非同期にリソースを解放するための構文です。
通常、ファイル、ストリーム、データベース接続、ネットワーク接続などのリソースは、使い終わったあとに明示的な解放処理が必要です。
C#では、このようなリソース解放にusingを使います。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
この場合、スコープを抜けるとDispose()が呼ばれます。
一方、リソース解放自体に非同期処理が必要な場合は、Dispose()ではなくDisposeAsync()を使います。そのDisposeAsync()を自動的に呼び出して待機してくれるのがawait usingです。
C#await using var resource = new AsyncResource();
このコードでは、スコープを抜けるタイミングで次のような処理が行われます。
C#await resource.DisposeAsync();
つまり、await usingは「非同期処理の中で使えるusing」ではなく、より正確には非同期Disposeを行うためのusingです。
1-2. C# 8.0で導入された非同期Disposeの仕組み
await usingは、C# 8.0で導入されました。
C# 8.0では、非同期ストリームやnullable参照型などとあわせて、非同期リソース解放の仕組みとしてIAsyncDisposableインターフェイスが追加されました。
従来のC#では、リソース解放の標準的な仕組みとしてIDisposableが使われていました。
C#public interface IDisposable
{
void Dispose();
}
しかし、Dispose()は戻り値がvoidであり、非同期処理をawaitできません。
そのため、リソース解放時に非同期処理が必要な場合でも、従来は同期的に待機したり、無理にブロッキングしたりする必要がありました。
そこで追加されたのがIAsyncDisposableです。
C#public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
IAsyncDisposableを実装した型は、await usingで安全に非同期破棄できます。
1-3. await usingで呼び出されるDisposeAsyncとは
await usingで呼び出されるのは、Dispose()ではなくDisposeAsync()です。
たとえば、次のようなクラスがあるとします。
C#public class AsyncResource : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await Task.Delay(100);
Console.WriteLine("非同期でリソースを破棄しました");
}
}
このクラスをawait usingで使うと、スコープを抜けるときにDisposeAsync()が呼び出されます。
C#await using var resource = new AsyncResource();
Console.WriteLine("リソースを使用中");
実行イメージは次のとおりです。
リソースを使用中
非同期でリソースを破棄しました
重要なのは、DisposeAsync()の完了を待ってから次の処理へ進む点です。
これにより、リソース解放が完了する前に後続処理が進んでしまう問題を防げます。
1-4. まず押さえるべき結論:usingはDispose、await usingはDisposeAsyncを呼ぶ
usingとawait usingの違いは、まず次のように理解するとわかりやすいです。
C#using var resource = new Resource();
これは、スコープ終了時に次の処理を行います。
C#resource.Dispose();
一方で、次のコードがあります。
C#await using var resource = new AsyncResource();
これは、スコープ終了時に次の処理を行います。
C#await resource.DisposeAsync();
つまり、基本的な違いは次のとおりです。
| 構文 | 呼び出されるメソッド | 対応インターフェイス | 処理 |
|---|---|---|---|
using | Dispose() | IDisposable | 同期的に破棄する |
await using | DisposeAsync() | IAsyncDisposable | 非同期で破棄する |
await usingを理解するうえでは、この違いを最初に押さえておくことが重要です。
2. await usingが必要になる背景
2-1. 従来のusingとIDisposableの役割
従来のC#では、リソース解放にはIDisposableとusingが使われてきました。
たとえば、ファイルを読み込む処理では次のように書きます。
C#using var reader = new StreamReader("sample.txt");
var text = reader.ReadToEnd();
このコードでは、readerが不要になったタイミングで自動的にDispose()が呼ばれます。
usingを使わずに書くと、次のようなtry-finallyに近い処理になります。
C#var reader = new StreamReader("sample.txt");
try
{
var text = reader.ReadToEnd();
}
finally
{
reader.Dispose();
}
usingの役割は、例外が発生しても確実にDispose()を呼び出すことです。
これにより、ファイルハンドル、メモリ、ネットワーク接続、データベース接続などのリソースを安全に解放できます。
2-2. 非同期処理ではDisposeだけでは不十分になる理由
非同期処理が一般的になると、リソースの解放処理にも非同期処理が必要になるケースが増えました。
たとえば、次のような処理です。
バッファに残ったデータを非同期でフラッシュする
ネットワーク越しに切断処理を通知する
データベース接続を非同期で閉じる
ストリームを非同期で閉じる
外部サービスとの通信終了処理を待つ
これらの処理は、I/O待ちを伴う可能性があります。
しかし、通常のDispose()は同期メソッドです。
C#void Dispose()
戻り値がvoidなので、内部で非同期処理を自然にawaitできません。
無理に非同期処理を同期的に待つと、次のような問題が起きることがあります。
スレッドをブロックしてしまう
パフォーマンスが低下する
デッドロックの原因になる
非同期処理の利点が失われる
破棄処理の完了タイミングが不明確になる
そのため、非同期処理に対応した破棄の仕組みが必要になりました。
2-3. ファイル・ネットワーク・DB接続などで非同期破棄が必要なケース
await usingが役立つ代表的なケースは、I/Oを伴うリソースです。
たとえば、ファイル操作では、書き込みバッファに残っているデータをストレージへ反映する処理が必要になることがあります。
C#await using var stream = new FileStream(
"sample.txt",
FileMode.Create,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
useAsync: true);
このような非同期対応のストリームでは、破棄時に内部の非同期処理を待機できる方が自然です。
また、データベース接続でも同様です。
C#await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
データベース接続の終了処理では、接続プールへの返却や通信の終了処理が発生する場合があります。こうした処理を非同期で行える型では、await usingを使うことで安全に後片付けできます。
ネットワーク通信やクラウドサービスのクライアントでも、切断処理や送信完了待ちが必要になる場合があります。
2-4. IAsyncDisposableが解決する課題
IAsyncDisposableは、非同期リソース解放のための標準インターフェイスです。
C#public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
このインターフェイスにより、C#では次のような書き方ができます。
C#await using var resource = new AsyncResource();
IAsyncDisposableが解決する主な課題は次のとおりです。
| 課題 | IAsyncDisposableによる解決 |
|---|---|
Dispose()ではawaitできない | DisposeAsync()で非同期に待てる |
| 破棄時のI/O処理を同期的に待つ必要がある | 非同期のまま完了を待てる |
| リソース解放の完了タイミングが曖昧になる | await usingで確実に待機できる |
| 独自の非同期クリーンアップ手段がばらつく | 標準インターフェイスとして統一できる |
つまり、IAsyncDisposableとawait usingは、非同期時代のリソース解放を安全に扱うための仕組みです。
3. await usingの基本的な書き方
3-1. await usingステートメントの構文
await usingには、ブロックを明示する書き方があります。
C#await using (var resource = new AsyncResource())
{
await resource.DoSomethingAsync();
}
この場合、{ }のブロックを抜けるときにDisposeAsync()が呼ばれます。
C#await resource.DisposeAsync();
例外が発生した場合でも、ブロックを抜けるときにDisposeAsync()が実行されます。
C#await using (var resource = new AsyncResource())
{
throw new InvalidOperationException();
}
この場合でも、resource.DisposeAsync()は呼び出されます。
通常のusingと同じように、await usingもリソースを確実に解放するための構文です。ただし、呼び出すメソッドがDisposeAsync()である点が異なります。
3-2. await using宣言の構文
C#では、using宣言と同じようにawait using宣言も使えます。
C#await using var resource = new AsyncResource();
await resource.DoSomethingAsync();
この書き方では、明示的なブロックはありません。
resourceは、現在のスコープを抜けるタイミングで破棄されます。
たとえば、メソッド内で次のように書いた場合です。
C#public async Task ExecuteAsync()
{
await using var resource = new AsyncResource();
await resource.DoSomethingAsync();
Console.WriteLine("処理完了");
}
この場合、ExecuteAsyncメソッドを抜ける直前にDisposeAsync()が呼び出されます。
await using宣言はコードが簡潔になるため、実務でもよく使われます。
3-3. ブロックを抜けたタイミングでDisposeAsyncが実行される流れ
await usingでは、リソースの使用範囲が終わったタイミングでDisposeAsync()が実行されます。
次の例を見てみます。
C#public async Task SampleAsync()
{
Console.WriteLine("開始");
await using (var resource = new AsyncResource())
{
Console.WriteLine("リソース使用中");
}
Console.WriteLine("終了");
}
AsyncResourceのDisposeAsync()が次のように実装されているとします。
C#public class AsyncResource : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await Task.Delay(100);
Console.WriteLine("DisposeAsync実行");
}
}
実行順は次のようになります。
開始
リソース使用中
DisposeAsync実行
終了
ポイントは、DisposeAsync()が完了してから終了が出力されることです。
つまり、await usingは単にDisposeAsync()を呼ぶだけでなく、その完了を待ちます。
3-4. try-finallyに展開した場合のイメージ
await usingは、内部的にはtry-finallyに近い形で考えると理解しやすいです。
次のコードがあるとします。
C#await using (var resource = new AsyncResource())
{
await resource.DoSomethingAsync();
}
これは、イメージとしては次のようなコードに近いです。
C#var resource = new AsyncResource();
try
{
await resource.DoSomethingAsync();
}
finally
{
if (resource != null)
{
await resource.DisposeAsync();
}
}
実際のコンパイラ変換はより複雑ですが、考え方としてはこのイメージで問題ありません。
つまり、await usingの役割は次の2つです。
スコープを抜けるときに確実に破棄処理を呼ぶ
破棄処理が非同期なら完了まで待つ
通常のusingもtry-finallyに展開されるイメージですが、await usingではfinally内でDisposeAsync()をawaitする点が違います。
3-5. await usingを使えるメソッドの条件
await usingを使うには、基本的にawaitが使える場所である必要があります。
つまり、通常はasyncメソッド内で使います。
C#public async Task ExecuteAsync()
{
await using var resource = new AsyncResource();
await resource.DoSomethingAsync();
}
戻り値は、一般的には次のいずれかになります。
C#Task
Task<T>
ValueTask
ValueTask<T>
たとえば、次のようなメソッドではawait usingを使えます。
C#public async Task<string> ReadAsync()
{
await using var resource = new AsyncResource();
return await resource.GetValueAsync();
}
一方、通常の同期メソッドではawaitが使えないため、await usingもそのままでは使えません。
C#public void Execute()
{
// await using は使えない
}
非同期破棄が必要な型を使う場合は、呼び出し元の設計も非同期にすることを検討しましょう。
4. IAsyncDisposableの使い方
4-1. IAsyncDisposableインターフェイスとは
IAsyncDisposableは、非同期でリソースを解放するためのインターフェイスです。
定義は次のようになっています。
C#public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
通常のIDisposableはDispose()を持ちます。
C#public interface IDisposable
{
void Dispose();
}
違いは、IAsyncDisposableのDisposeAsync()がValueTaskを返すことです。
これにより、破棄処理の中で非同期処理を行えます。
C#public async ValueTask DisposeAsync()
{
await CloseConnectionAsync();
}
IAsyncDisposableを実装した型は、await usingで利用できます。
C#await using var resource = new AsyncResource();
4-2. DisposeAsyncメソッドの実装例
簡単なIAsyncDisposableの実装例を見てみましょう。
C#public sealed class AsyncResource : IAsyncDisposable
{
private bool _disposed;
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
await Task.Delay(100);
Console.WriteLine("非同期リソースを解放しました");
_disposed = true;
}
}
このクラスは、DisposeAsync()が呼ばれると非同期で少し待機し、リソース解放のメッセージを出力します。
使用する側は次のように書けます。
C#public async Task RunAsync()
{
await using var resource = new AsyncResource();
Console.WriteLine("リソースを使用しています");
}
実行結果のイメージは次のとおりです。
リソースを使用しています
非同期リソースを解放しました
await usingを使うことで、呼び出し側はDisposeAsync()を明示的に呼ばなくても、安全に非同期破棄できます。
4-3. ValueTaskを返す理由
DisposeAsync()の戻り値はTaskではなくValueTaskです。
C#ValueTask DisposeAsync();
ValueTaskは、非同期処理が必ずしも毎回発生しない場合に有効です。
リソース破棄処理では、次のようなケースがあります。
すでに破棄済みなので何もしない
非同期処理が不要で即座に完了する
条件によっては非同期I/Oが発生する
一部の処理だけ非同期になる
このように、破棄処理が同期的に完了することも多いため、TaskよりもValueTaskの方が効率的な場合があります。
たとえば、即座に完了する場合は次のように書けます。
C#public ValueTask DisposeAsync()
{
Console.WriteLine("同期的に完了する破棄処理");
return ValueTask.CompletedTask;
}
一方、非同期処理が必要な場合はasync ValueTaskとして実装できます。
C#public async ValueTask DisposeAsync()
{
await FlushAsync();
}
ただし、ValueTaskはTaskより扱いに注意が必要な場面もあります。通常の利用では、DisposeAsync()の戻り値を複数回awaitしたり保存したりせず、await usingに任せるのが安全です。
4-4. IDisposableとIAsyncDisposableを両方実装するパターン
型によっては、IDisposableとIAsyncDisposableの両方を実装することがあります。
C#public sealed class HybridResource : IDisposable, IAsyncDisposable
{
private bool _disposed;
public void Dispose()
{
if (_disposed)
{
return;
}
Console.WriteLine("同期的にリソースを解放しました");
_disposed = true;
}
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
await Task.Delay(100);
Console.WriteLine("非同期でリソースを解放しました");
_disposed = true;
}
}
このクラスは、usingでもawait usingでも使えます。
C#using var syncResource = new HybridResource();
この場合はDispose()が呼ばれます。
C#await using var asyncResource = new HybridResource();
この場合はDisposeAsync()が呼ばれます。
両方実装する理由は、同期APIと非同期APIの両方に対応するためです。
ただし、両方を実装すると設計が複雑になるため、必要性を考えて実装することが大切です。
4-5. 非同期リソース解放処理を書くときの注意点
DisposeAsync()を書くときには、いくつか注意点があります。
まず、複数回呼ばれても問題ないようにします。
C#private bool _disposed;
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
_disposed = true;
await ReleaseAsync();
}
DisposeAsync()は、利用者が明示的に呼ぶ可能性もありますし、await usingで自動的に呼ばれる可能性もあります。そのため、二重解放を防ぐ設計が必要です。
次に、DisposeAsync()内で重い同期処理を行わないようにします。
C#public async ValueTask DisposeAsync()
{
await FlushAsync();
await CloseAsync();
}
非同期破棄の目的は、I/O待ちを非同期で扱うことです。内部で重い同期処理を実行してしまうと、await usingを使うメリットが薄れてしまいます。
また、DisposeAsync()内で例外が発生する可能性にも注意が必要です。破棄処理で例外が出ると、元の処理で発生した例外を覆い隠してしまう可能性があります。
5. usingとawait usingの違い
5-1. usingはDisposeを同期的に呼び出す
通常のusingは、IDisposable.Dispose()を同期的に呼び出します。
C#using var resource = new SyncResource();
このコードでは、スコープを抜けるときに次の処理が行われます。
C#resource.Dispose();
Dispose()は同期メソッドです。
C#public void Dispose()
{
Console.WriteLine("同期的に破棄");
}
そのため、Dispose()内の処理が完了するまで現在のスレッドは待機します。
ファイルハンドルを閉じる、メモリを解放する、軽い後片付けを行うといった処理であれば、通常のusingで十分です。
5-2. await usingはDisposeAsyncを非同期に待機する
await usingは、IAsyncDisposable.DisposeAsync()を呼び出し、その完了をawaitします。
C#await using var resource = new AsyncResource();
このコードでは、スコープを抜けるときに次の処理が行われます。
C#await resource.DisposeAsync();
DisposeAsync()は非同期メソッドとして実装できます。
C#public async ValueTask DisposeAsync()
{
await Task.Delay(100);
Console.WriteLine("非同期で破棄");
}
このように、破棄処理にI/O待ちや非同期通信が含まれる場合は、await usingを使うことで自然に待機できます。
5-3. IDisposableとIAsyncDisposableの違い
IDisposableとIAsyncDisposableの違いを整理すると、次のようになります。
| 項目 | IDisposable | IAsyncDisposable |
|---|---|---|
| メソッド | Dispose() | DisposeAsync() |
| 戻り値 | void | ValueTask |
| 呼び出す構文 | using | await using |
| 非同期処理 | 直接awaitできない | awaitできる |
| 主な用途 | 同期的なリソース解放 | 非同期のリソース解放 |
IDisposableは昔からある標準的なリソース解放の仕組みです。
一方、IAsyncDisposableは、非同期処理が必要なリソース解放に対応するための仕組みです。
どちらが優れているというより、用途が異なります。
5-4. 処理の流れをコードで比較
usingとawait usingの違いをコードで比較してみましょう。
まず、通常のusingです。
C#public sealed class SyncResource : IDisposable
{
public void Dispose()
{
Console.WriteLine("Disposeが呼ばれました");
}
}
public void Run()
{
using var resource = new SyncResource();
Console.WriteLine("処理中");
}
実行結果のイメージです。
処理中
Disposeが呼ばれました
次に、await usingです。
C#public sealed class AsyncResource : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await Task.Delay(100);
Console.WriteLine("DisposeAsyncが呼ばれました");
}
}
public async Task RunAsync()
{
await using var resource = new AsyncResource();
Console.WriteLine("処理中");
}
実行結果のイメージです。
処理中
DisposeAsyncが呼ばれました
見た目は似ていますが、呼び出されるメソッドが異なります。
C#using → Dispose()
await using → DisposeAsync()
この違いが、usingとawait usingを使い分ける最大のポイントです。
5-5. どちらを使うべきか判断する基準
usingとawait usingのどちらを使うべきかは、対象の型がどのインターフェイスを実装しているかで判断します。
対象の型がIDisposableのみを実装している場合は、通常のusingを使います。
C#using var resource = new SyncResource();
対象の型がIAsyncDisposableを実装しており、非同期で破棄したい場合は、await usingを使います。
C#await using var resource = new AsyncResource();
両方を実装している場合は、破棄処理を同期的に行いたいか、非同期で行いたいかを考えます。
基本的な判断基準は次のとおりです。
| 状況 | 使う構文 |
|---|---|
| 同期的なリソース解放で十分 | using |
| 破棄処理で非同期I/Oが発生する | await using |
対象がIDisposableのみ | using |
対象がIAsyncDisposableのみ | await using |
| 非同期メソッド内で非同期破棄したい | await using |
実務では、利用するクラスのドキュメントや実装を確認し、IAsyncDisposableに対応しているかを見て判断するとよいでしょう。
6. await usingの実践例
6-1. IAsyncDisposableを実装したクラスのサンプル
ここでは、実際にIAsyncDisposableを実装したクラスを作ってみます。
C#public sealed class SampleAsyncResource : IAsyncDisposable
{
private bool _disposed;
public async Task UseAsync()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(SampleAsyncResource));
}
Console.WriteLine("リソースを使用しています");
await Task.Delay(100);
}
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
Console.WriteLine("非同期破棄を開始します");
await Task.Delay(100);
Console.WriteLine("非同期破棄が完了しました");
_disposed = true;
}
}
このクラスには、非同期でリソースを使うUseAsync()と、非同期で破棄するDisposeAsync()があります。
_disposedフラグを使って、破棄済みのリソースが再利用されないようにしています。
6-2. await usingで非同期リソースを安全に破棄する例
先ほどのSampleAsyncResourceをawait usingで使ってみます。
C#public async Task RunAsync()
{
await using var resource = new SampleAsyncResource();
await resource.UseAsync();
Console.WriteLine("メソッドを終了します");
}
実行結果のイメージは次のようになります。
リソースを使用しています
メソッドを終了します
非同期破棄を開始します
非同期破棄が完了しました
RunAsync()メソッドを抜ける直前にDisposeAsync()が呼ばれていることがわかります。
明示的に次のように書く必要はありません。
C#await resource.DisposeAsync();
await usingを使えば、スコープ終了時に自動で呼び出されます。
6-3. FileStreamやDB接続で使うイメージ
await usingは、実務ではファイルやDB接続のようなI/Oリソースで使われることが多いです。
たとえば、非同期でファイルを書き込む場合です。
C#public async Task WriteFileAsync()
{
await using var stream = new FileStream(
"sample.txt",
FileMode.Create,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
useAsync: true);
var bytes = System.Text.Encoding.UTF8.GetBytes("Hello, await using!");
await stream.WriteAsync(bytes, 0, bytes.Length);
}
この例では、streamのスコープ終了時に非同期破棄が行われます。
データベース接続でも同じような考え方ができます。
C#public async Task ExecuteDbAsync(string connectionString)
{
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = "SELECT COUNT(*) FROM Users";
var count = await command.ExecuteScalarAsync();
Console.WriteLine(count);
}
このように、接続やコマンドなどがIAsyncDisposableに対応している場合は、await usingを使うことで非同期に安全な後片付けができます。
6-4. await using varを使った簡潔な書き方
await usingは、ブロック形式だけでなく、await using varとして簡潔に書けます。
C#await using var resource = new SampleAsyncResource();
await resource.UseAsync();
この書き方では、現在のスコープを抜けるときにDisposeAsync()が呼び出されます。
たとえば、次のようなメソッドがあります。
C#public async Task ProcessAsync()
{
await using var resource1 = new SampleAsyncResource();
await using var resource2 = new SampleAsyncResource();
await resource1.UseAsync();
await resource2.UseAsync();
Console.WriteLine("処理完了");
}
この場合、ProcessAsync()メソッドを抜けるタイミングでresource1とresource2が破棄されます。
コードがすっきりするため、スコープ全体でリソースを使う場合にはawait using varが便利です。
一方で、リソースの使用範囲を明確に限定したい場合は、ブロック形式を使うとよいでしょう。
C#await using (var resource = new SampleAsyncResource())
{
await resource.UseAsync();
}
6-5. DisposeAsyncが呼ばれる順番を確認する
複数のawait using宣言を使った場合、破棄される順番にも注意が必要です。
通常、同じスコープで複数のリソースを宣言した場合、後に宣言したものから先に破棄されます。
C#public async Task RunAsync()
{
await using var resource1 = new NamedAsyncResource("Resource 1");
await using var resource2 = new NamedAsyncResource("Resource 2");
Console.WriteLine("処理中");
}
クラスは次のようにします。
C#public sealed class NamedAsyncResource : IAsyncDisposable
{
private readonly string _name;
public NamedAsyncResource(string name)
{
_name = name;
}
public async ValueTask DisposeAsync()
{
await Task.Delay(100);
Console.WriteLine($"{_name} を破棄しました");
}
}
実行結果のイメージです。
処理中
Resource 2 を破棄しました
Resource 1 を破棄しました
後から作成したリソースが先に破棄されることで、依存関係を安全に扱いやすくなります。
たとえば、connectionの上にcommandがある場合、先にcommandを破棄し、その後でconnectionを破棄する方が自然です。
7. await usingでよくある疑問とエラー
7-1. await usingは何をawaitしているのか
await usingがawaitしているのは、リソースの使用処理ではありません。
await usingが待っているのは、スコープ終了時に呼び出されるDisposeAsync()です。
次のコードを見てください。
C#await using var resource = new AsyncResource();
await resource.DoSomethingAsync();
このうち、await usingのawaitは、初期化処理やコンストラクターをawaitしているわけではありません。
イメージとしては、スコープ終了時に次の処理を行っています。
C#await resource.DisposeAsync();
つまり、await usingのawait対象は、破棄処理です。
ここを誤解すると、「await usingを使えばコンストラクターを非同期にできる」と考えてしまいがちですが、それは正しくありません。
7-2. await usingはコンストラクターを非同期化するものではない
C#のコンストラクターはasyncにできません。
C#public async MyClass()
{
// これはできない
}
await usingを使っても、コンストラクターが非同期になるわけではありません。
次のコードを見てください。
C#await using var resource = new AsyncResource();
このコードでawaitされるのは、new AsyncResource()ではありません。
new AsyncResource()は通常どおり同期的に実行されます。
awaitされるのは、スコープ終了時のDisposeAsync()です。
非同期の初期化が必要な場合は、ファクトリメソッドを用意することがあります。
C#public sealed class AsyncResource : IAsyncDisposable
{
private AsyncResource()
{
}
public static async Task<AsyncResource> CreateAsync()
{
var resource = new AsyncResource();
await resource.InitializeAsync();
return resource;
}
private Task InitializeAsync()
{
return Task.Delay(100);
}
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
}
}
使用側は次のようになります。
C#await using var resource = await AsyncResource.CreateAsync();
この場合、初期化はCreateAsync()でawaitし、破棄はawait usingでDisposeAsync()を待機します。
7-3. IAsyncDisposableを実装していない型で使えるか
await usingを使うには、基本的に対象の型がIAsyncDisposableを実装している必要があります。
C#public sealed class AsyncResource : IAsyncDisposable
{
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
}
}
このような型であれば、次のように使えます。
C#await using var resource = new AsyncResource();
一方、IDisposableだけを実装している型に対してawait usingを使うと、コンパイルエラーになります。
C#public sealed class SyncResource : IDisposable
{
public void Dispose()
{
}
}
この型に対して次のようには書けません。
C#await using var resource = new SyncResource(); // エラー
IDisposableの型には通常のusingを使います。
C#using var resource = new SyncResource();
7-4. DisposeAsyncが呼ばれないケース
await usingを使っていれば、通常はスコープ終了時にDisposeAsync()が呼ばれます。
ただし、次のようなケースには注意が必要です。
まず、リソースをawait usingで管理していない場合です。
C#var resource = new AsyncResource();
// DisposeAsyncは自動では呼ばれない
この場合、手動で呼び出す必要があります。
C#await resource.DisposeAsync();
次に、プロセスが強制終了した場合です。アプリケーションがクラッシュしたり、プロセスが強制的に停止されたりした場合、通常のスコープ終了処理が実行されないことがあります。
また、DisposeAsync()を実装していても、使用側がusingを使っている場合はDisposeAsync()ではなくDispose()が呼ばれる可能性があります。両方を実装している型では、どちらの構文を使うかが重要です。
C#using var resource = new HybridResource(); // Dispose()
await using var asyncResource = new HybridResource(); // DisposeAsync()
非同期破棄を期待する場合は、必ずawait usingを使いましょう。
7-5. ConfigureAwaitとの組み合わせ方
ライブラリ開発などでは、await usingとConfigureAwait(false)を組み合わせたい場合があります。
通常のawaitでは、次のように書きます。
C#await SomeAsync().ConfigureAwait(false);
await usingでも、対象が対応していれば次のように書けます。
C#await using (resource.ConfigureAwait(false))
{
await resource.DoSomethingAsync().ConfigureAwait(false);
}
また、await using varで使う場合は、書き方に注意が必要です。
C#await using var resource = new AsyncResource();
この書き方では、DisposeAsync()に対するConfigureAwait(false)を直接見せにくい場合があります。
ライブラリコードでコンテキストのキャプチャを避けたい場合は、ブロック形式でConfigureAwait(false)を使うことを検討します。
C#var resource = new AsyncResource();
await using (resource.ConfigureAwait(false))
{
await resource.DoSomethingAsync().ConfigureAwait(false);
}
アプリケーションコードでは必ずしも必要ではありませんが、ライブラリ設計では意識しておくとよいポイントです。
7-6. Visual Studioがawait usingを提案する理由
Visual StudioやIDEがawait usingを提案することがあります。
これは、対象の型がIAsyncDisposableを実装しているためです。
たとえば、ある型が次のように定義されているとします。
C#public sealed class AsyncResource : IAsyncDisposable
{
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
}
}
この型を使っているとき、IDEは「このリソースは非同期破棄できます」と判断し、await usingを提案する場合があります。
特に、非同期メソッド内でIAsyncDisposableを実装した型を使っている場合、DisposeAsync()を確実に呼ぶためにawait usingを使うのが自然です。
IDEの提案は、リソースリークを防ぐためのヒントとして役立ちます。
ただし、すべてのケースで機械的にawait usingへ変更すればよいわけではありません。対象の型がどのような破棄処理を行うのか、同期破棄と非同期破棄のどちらが適切なのかを確認しましょう。
8. await usingを使うときのベストプラクティス
8-1. 非同期破棄が必要な型だけにawait usingを使う
await usingは便利ですが、何でもawait usingにすればよいわけではありません。
基本的には、対象の型がIAsyncDisposableを実装していて、非同期破棄が必要な場合に使います。
C#await using var resource = new AsyncResource();
同期的な破棄で十分な型には、通常のusingを使います。
C#using var resource = new SyncResource();
await usingを使うと、スコープ終了時に非同期処理が発生する可能性があります。そのため、不要な場面で使うとコードの意図がわかりにくくなることがあります。
判断に迷った場合は、次の基準で考えるとよいでしょう。
型が
IAsyncDisposableを実装しているか破棄時に非同期I/Oが発生するか
非同期メソッド内で利用しているか
ライブラリやフレームワークが
await usingを推奨しているか
8-2. IDisposableとIAsyncDisposableの実装方針を決める
自作クラスでリソース解放を設計する場合、IDisposableとIAsyncDisposableのどちらを実装するかを決める必要があります。
同期的なリソース解放だけで十分なら、IDisposableで問題ありません。
C#public sealed class SyncResource : IDisposable
{
public void Dispose()
{
// 同期的な破棄処理
}
}
非同期I/Oを伴う破棄処理が必要なら、IAsyncDisposableを実装します。
C#public sealed class AsyncResource : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await ReleaseAsync();
}
}
同期APIと非同期APIの両方を提供したい場合は、両方を実装することもあります。
C#public sealed class Resource : IDisposable, IAsyncDisposable
{
public void Dispose()
{
// 同期破棄
}
public async ValueTask DisposeAsync()
{
// 非同期破棄
await ReleaseAsync();
}
}
ただし、両方を実装する場合は、二重解放や状態管理に注意が必要です。
8-3. 例外発生時でもリソースが解放されるようにする
await usingの大きなメリットは、例外が発生してもDisposeAsync()が呼ばれることです。
C#public async Task RunAsync()
{
await using var resource = new AsyncResource();
throw new InvalidOperationException();
}
この場合でも、スコープを抜けるときにDisposeAsync()が実行されます。
これは、await usingがtry-finallyに近い仕組みで動作するためです。
ただし、DisposeAsync()内でも例外が発生する可能性があります。
C#public async ValueTask DisposeAsync()
{
await CloseAsync(); // ここで例外が発生する可能性
}
破棄処理で例外が発生すると、元の例外処理に影響することがあります。
そのため、DisposeAsync()内では、必要に応じて例外をログに記録したり、再スローすべきかを慎重に判断したりすることが重要です。
8-4. DisposeAsync内で重い同期処理を避ける
DisposeAsync()の中で重い同期処理を行うと、非同期破棄のメリットが失われます。
避けたい例は次のようなコードです。
C#public ValueTask DisposeAsync()
{
Thread.Sleep(5000);
return ValueTask.CompletedTask;
}
このコードは非同期メソッドのように見えますが、実際にはスレッドをブロックしています。
非同期破棄では、I/O待ちをawaitするように書きます。
C#public async ValueTask DisposeAsync()
{
await FlushAsync();
await CloseAsync();
}
また、CPU負荷の高い処理をDisposeAsync()に詰め込みすぎるのも避けるべきです。
DisposeAsync()は、あくまでリソース解放のためのメソッドです。通常の処理ロジックや重い計算処理を含めると、コードの見通しが悪くなります。
8-5. ライブラリ設計でIAsyncDisposableを採用すべきケース
ライブラリや共通部品を設計する場合、次のような型ではIAsyncDisposableの採用を検討するとよいでしょう。
非同期ストリームを内部で保持する型
データベース接続やトランザクションを扱う型
ネットワーク接続を管理する型
外部APIクライアントを管理する型
メッセージキューやソケットを扱う型
破棄時にフラッシュや送信完了待ちが必要な型
たとえば、内部で非同期ストリームを保持している場合は、外側のクラスもIAsyncDisposableを実装するのが自然です。
C#public sealed class MyWriter : IAsyncDisposable
{
private readonly Stream _stream;
public MyWriter(Stream stream)
{
_stream = stream;
}
public async ValueTask DisposeAsync()
{
await _stream.DisposeAsync();
}
}
このようにすることで、利用者は次のように書けます。
C#await using var writer = new MyWriter(stream);
ライブラリ利用者にとって、await usingで安全にリソースを解放できる設計は扱いやすいです。
まとめ
await usingは、C#で非同期にリソースを破棄するための構文です。
通常のusingがDispose()を呼び出すのに対して、await usingはDisposeAsync()を呼び出し、その完了をawaitします。
C#using → Dispose()
await using → DisposeAsync()
await usingを使うには、対象の型が基本的にIAsyncDisposableを実装している必要があります。
C#public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
非同期破棄が必要になるのは、ファイル、ネットワーク、データベース接続、ストリームなど、I/Oを伴うリソースを扱う場面です。
await usingを使うことで、次のようなメリットがあります。
| メリット | 内容 |
|---|---|
| 非同期で破棄できる | DisposeAsync()をawaitできる |
| 例外時も安全 | スコープ終了時に破棄される |
| コードが簡潔 | 手動でDisposeAsync()を書く必要がない |
| I/O待ちに強い | スレッドをブロックしにくい |
一方で、同期的なリソース解放で十分な型には、通常のusingを使えば問題ありません。
C#using var resource = new SyncResource();
非同期破棄が必要な型には、await usingを使います。
C#await using var resource = new AsyncResource();
C#のawait usingを理解するポイントは、難しく考えすぎず、まずは次の一文で押さえることです。
usingはDisposeを呼び、await usingはDisposeAsyncを呼ぶ。
この違いを理解しておけば、IAsyncDisposableを実装した型や、非同期リソースを扱うコードをより安全に書けるようになります。

