本文作者——句幽

.NET Core 3.0的版本更新中,官方我們帶來了一個新的介面 IAsyncDisposable

小夥伴一看肯定就知道,它和.NET中原有的IDisposable介面肯定有著密不可分分的關係,且一定是它的非同步實現版本。

那麼.NET是為什麼要在 .NET Core 3.0 (伴隨C# 8) 釋出的同時,帶來該介面呢? 還有就是該非同步版本和原來的IDispose有著什麼樣的區別呢? 到底在哪種場景下我們能使用它呢?

帶著這些問題,我們今天一起來認識一下這位"新朋友" —— IAsyncDisposable

為了更好的瞭解它,讓我們先來回顧一下.NET中的資源釋放:

.NET的資源釋放

由於.NET強大的GC,對於託管資源來說(比如C#的類例項),它的釋放往往不需要開發人員來操心。

但是在開發過程中,有時候我們需要涉及到非託管的資源,比如I/O操作,將緩衝區中的文字內容儲存到檔案中、網路通訊,傳送資料包等等。

由於這些操作GC沒有辦法控制,所以也就沒有辦法來管理它們的生命週期。如果使用了非託管資源之後,沒有及時進行釋放資源,那麼就會造成記憶體的洩漏問題。

而.NET為我們提供了一些手段來進行資源釋放的操作:

解構函式

解構函式在C#中是一個語法糖,在建構函式前方加一個符號即代表使用解構函式 。

public class ExampleClass
{
public ExampleClass()
{
} ~ExampleClass() // 解構函式
{
// 釋放非託管資源
}
}

當一個類申明瞭析構函數了之後,GC將會對它進行特殊的處理,當該例項的資源被GC回收之前會呼叫解構函式。(該部分內容本文將不做過多介紹)

雖然解構函式方法在某些需要進行清理的情況下是有效的,但它有下面兩個嚴重的缺點:

  • 只有在GC檢測到某個物件可以被回收時才會呼叫該物件的終結方法,這發生在不再需要資源之後的某個不確定的時間。這樣一來,開發人員可以或希望釋放資源的時刻與資源實際被終結方法釋放的時刻之間會有一個延遲。如果程式需要使用許多稀缺資源(容易耗盡的資源)或不釋放資源的代價會很高(例如,大塊的非託管記憶體),那麼這樣的延遲可能會讓人無法接受。
  • 當CLR需要呼叫終結方法時,它必須把回收物件記憶體的工作推遲到垃圾收集的下一輪(終結方法會在兩輪垃圾收集之間執行)。這意味著物件的記憶體會在很長一段時間內得不到釋放。

因此,如果需要儘快回收非託管資源,或者資源很稀缺,或者對效能要求極高以至於無法接受在GC時增加額外開銷,那麼在這些情況下完全依靠解構函式的方法可能不太合適。

而框架提供了IDisposable介面,該介面為開發人員提供了一種手動釋放非託管資源的方法,可以用來立即釋放不再需要的非託管資源。

IDisposable

.NET Framework 1.1開始 ,.NET就為我們提供了IDispose介面。

使用該介面,我們可以實現名為Dispose的方法,進行一些手動釋放資源的操作(包括託管資源和非託管資源)。

public class ExampleClass:IDisposable
{
private Stream _memoryStream = new MemoryStream(); public ExampleClass()
{
} public void Dispose()
{
// 釋放資源
myList.Clear();
myData = null;
_memoryStream.Dispose();
}
}

在C#中,我們除了可以手動呼叫 xx.Dispose()方法來觸發釋放之外,還可以使用using的語法糖。

當我們在 visual studio 中新增IDisposable介面時,它會提示我們使用是否使用“釋放模式”:

“釋放模式”所生成的程式碼如下:

protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: 釋放託管狀態(託管物件)
} // TODO: 釋放未託管的資源(未託管的物件)並重寫終結器
// TODO: 將大型欄位設定為 null
disposedValue = true;
}
} // // TODO: 僅當“Dispose(bool disposing)”擁有用於釋放未託管資源的程式碼時才替代終結器
// ~ExampleClass()
// {
// // 不要更改此程式碼。請將清理程式碼放入“Dispose(bool disposing)”方法中
// Dispose(disposing: false);
// } public void Dispose()
{
// 不要更改此程式碼。請將清理程式碼放入“Dispose(bool disposing)”方法中
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

釋放資源的程式碼被放置在 Dispose(bool disposing) 方法中,你可以選用 解構函式 或者 IDisposable 來進行呼叫該方法。

這裡說一下:在 IDisposable 的實現中,有一句 GC.SuppressFinalize(this);。 這句話的意思是,告訴GC,不需要對該類的解構函式進行單獨處理了。也就是說,該類的解構函式將不會被呼叫。因為資源已經在 Dispose() 中被我清理了。

非同步時代

.NET Core開始,就意味著.NET來到了一個全新的非同步時代。無論是各種基礎類庫(比如System.IO)、AspNet Core、還是EFCore..... 它們都支援非同步操作,應該說是推薦非同步操作。

在今天,假如一個新專案沒有使用 awaitasync。你都會覺得自己在寫假程式碼

現在越來越多的開發者都愛上了這種非同步方式:不阻止執行緒的執行,帶來高效能的同時還完全不需要更改原有的編碼習慣,可謂是兩全其美。

所以從.NET Core 開始到現在的.NET 5 ,每一次版本更迭都會有一批API提供了非同步的版本。

IAsyncDisposable的誕生

為了提供這樣一種機制讓使用者能夠執行資源密集型的處置操作,而不會長期阻塞GUI應用程式的主執行緒,我們讓操作成為了非同步。

同樣,釋放資源的時候我們能否成為非同步呢? 假如一次釋放操作會佔耗費太多的時間,那為什麼我們不讓它去非同步執行呢?

為了解決這一問題,同時更好的完善.NET非同步程式設計的體驗,IAsyncDisposable誕生了。

它的用法與IDisposable非常的類似:

public class ExampleClass : IAsyncDisposable
{
private Stream _memoryStream = new MemoryStream(); public ExampleClass()
{ } public async ValueTask DisposeAsync()
{
await _memoryStream.DisposeAsync();
}
}

當然,using的語法糖同樣適用於它。不過,由於它是非同步程式設計的風格,在使用時記得新增await關鍵字:

await using var s = new ExampleClass()
{
// doing
};

當然在 C# 8 以上,我們可以使用using作用域的簡化寫法:

await using var s = new ExampleClass();
// doing

IAsyncDisposable與IDisposable的選擇

有一個關鍵點是: IAsyncDisposable 其實並沒有繼承於 IDisposable

這就意味著,我們可以選擇兩者中的任意一個,或者同時都要。

那麼我們到底該選擇哪一個呢?

這個問題其實很類似於EF剛為大家提供SaveChangesAsync方法的時候,到底我們該選用SaveChangesAsync還是SaveChanges呢?

在以往同步版本的程式碼中,我們往往會選擇SaveChanges同步方法。 當來到了非同步的環境,我們往往會選擇SaveChangesAsync

所以在AspNet Core這個全流程非同步的大環境下,我們的程式碼潛移默化的就會更改為SaveChangesAsync

IAsyncDisposable也是同理的,當我們處於非同步的環境中,所使用的資源提供了非同步釋放的介面,那麼我們肯定就會自然而然的使用IAsyncDisposable

.NET 5 之後,大部分的類都具有了IAsyncDisposable的實現。比如:

  • Utf8JsonWriterStreamWriter這些與檔案操作有關的類;
  • DbContext這類資料庫操作類
  • Timer
  • 依賴注入的ServiceProvider
  • ………………

接下來的.NET版本中,我們也會看到AspNet Core中的Controller 等對於IAsyncDisposable提供支援。

可以預測是,在未來的.NET發展中,全非同步的發展是必然的。後面越來越的已有庫會支援非同步的所有操作,包括IAsyncDisposable的使用也會越來越頻繁。

Asp Net Core 依賴注入中的IAsyncDisposable

對於咱們使用AspNet Core的開發人員來說,我們在大多數情況下都會依賴於框架所提供的依賴注入功能。

而依賴注入框架,會在作用域釋放的時候,自動去呼叫所注入服務的釋放介面IDisposable

比如我們把 DbContext 注入之後,其實就只管使用就行了,從來不會關心它的Dispose問題。 相對於傳統using(var dbContext = new MyDbContext)的方式要省心很多,也不會擔心忘記寫釋放而導致的資料庫連線未釋放的問題。

那麼,當IAsyncDisposable出現之後呢?會出現什麼情況:

public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(); services.AddScoped<DemoDisposableObject>(); // 注入測試類
} public class DemoDisposableObject : IAsyncDisposable
{
public ValueTask DisposeAsync()
{
code here
// 當完成一次http 請求後,該方法會自動呼叫
}
}

當我們實現了IAsyncDisposable之後,會被自動呼叫。

那麼如果 IAsyncDisposableIDisposable 一同使用呢?

public class DemoDisposableObject : IAsyncDisposable,IDisposable
{
public void Dispose()
{
code here
} public ValueTask DisposeAsync()
{
code here
}
}

這樣的結果是:只有DisposeAsync方法會被呼叫

為什麼會有這樣的結果呢? 讓我們一起來扒開它的面紗。

以下程式碼位於 AspNet Core原始碼

public class RequestServicesFeature : IServiceProvidersFeature, IDisposable, IAsyncDisposable
{
public IServiceProvider RequestServices
{
get
{
if (!_requestServicesSet && _scopeFactory != null)
{
_scope = _scopeFactory.CreateScope();
……………………
}
return _requestServices!;
}
} public ValueTask DisposeAsync()
{
switch (_scope)
{
case IAsyncDisposable asyncDisposable:
var vt = asyncDisposable.DisposeAsync();
………………
break;
case IDisposable disposable:
disposable.Dispose();
break;
} ……………………
return default;
} public void Dispose()
{
DisposeAsync().AsTask().GetAwaiter().GetResult();
}
}

為了方便起見,我省略了部分程式碼。 這裡的關鍵程式碼在於: DisposeAsync()方法,它會在內部進行判斷,IServiceScope是否為IAsyncDisposable型別。如果是,則會採用它的IServiceScope的非同步釋放方法。

所以本質上還是回到了官方依賴注入框架中IServiceScope的實現:

以下程式碼位於 DependencyInjection原始碼

internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvider, IAsyncDisposable, IServiceScopeFactory
{
public ValueTask DisposeAsync()
{
List<object> toDispose = BeginDispose(); if (toDispose != null)
{
try
{
for (int i = toDispose.Count - 1; i >= 0; i--)
{
object disposable = toDispose[i];
if (disposable is IAsyncDisposable asyncDisposable)
{
ValueTask vt = asyncDisposable.DisposeAsync();
if (!vt.IsCompletedSuccessfully)
{
return Await(i, vt, toDispose);
} // If its a IValueTaskSource backed ValueTask,
// inform it its result has been read so it can reset
vt.GetAwaiter().GetResult();
}
else
{
((IDisposable)disposable).Dispose();
}
}
}
catch (Exception ex)
{
return new ValueTask(Task.FromException(ex));
}
} return default;
}
}

可以看出新版本的IServiceScope實現一定是繼承了IAsyncDisposable介面,所以在上面的AspNet Core的程式碼裡,它一定會呼叫IServiceScopeDisposeAsync()方法。

IServiceScope的預設實現在非同步釋放時會進行判斷:如果注入的例項為IAsyncDisposable則呼叫DisposeAsync(),否則判斷是否為IDisposable

這也解釋了為什麼我們在上面同時實現兩個釋放介面,卻只有非同步版本的會被呼叫。

總結

在上面的文章中,我們瞭解到IAsyncDisposable作為.NET非同步發展中一個重要的新介面,在應用上會被越來越頻繁的使用,它將逐步完善.NET的非同步生態。

當存在下方的情況時,我們應該優先考慮來使用它:

  • 當內部擁有的資源具有對IAsyncDisposable的實現(比如Utf8JsonWriter等),我們可以採用使用IAsyncDisposable來對他們進行釋放。
  • 當在非同步的大環境下,新編寫一個需要釋放資源的類,可以優先考慮使用IAsyncDisposable

現在.NET的很多類庫都已經同時支援了IDisposableIAsyncDisposable。而從使用者的角度來看,其實呼叫任何一個釋放方法都能夠達到釋放資源的目的。就好比DbContextSaveChangesSaveChangesAsync

但是從未來的發展角度來看,IAsyncDisposable會成使用的更加頻繁。因為它應該能夠優雅地處理託管資源,而不必擔心死鎖。

而對於現在已有程式碼中實現了IDisposable的類,如果想要使用IAsyncDisposable。建議您同時實現兩個介面,已保證使用者在使用時,無論呼叫哪個介面都能達到效果,而達到相容性的目的。

類似於下方程式碼:

節選自Stream類的原始碼

public void Dispose() => Close();

public virtual void Close()
{ Dispose(true);
GC.SuppressFinalize(this);
} public virtual ValueTask DisposeAsync()
{
try
{
Dispose();
return default;
}
catch (Exception exc)
{
return ValueTask.FromException(exc);
}
}

最後的最後,希望 點贊,關注,一鍵三連 走一波。