詳解C#中IAsyncDisposable接口的使用
在.NET Core 3.0
的版本更新中,官方我們帶來了一個(gè)新的接口 IAsyncDisposable
。
小伙伴一看肯定就知道,它和.NET中原有的IDisposable
接口肯定有著密不可分分的關(guān)系,且一定是它的異步實(shí)現(xiàn)版本。
那么.NET是為什么要在 .NET Core 3.0 (伴隨C# 8) 發(fā)布的同時(shí),帶來該接口呢? 還有就是該異步版本和原來的IDispose
有著什么樣的區(qū)別呢? 到底在哪種場景下我們能使用它呢?
帶著這些問題,我們今天一起來認(rèn)識(shí)一下這位"新朋友" —— IAsyncDisposable
。
為了更好的了解它,讓我們先來回顧一下.NET中的資源釋放:
.NET的資源釋放
由于.NET強(qiáng)大的GC,對于托管資源來說(比如C#的類實(shí)例),它的釋放往往不需要開發(fā)人員來操心。
但是在開發(fā)過程中,有時(shí)候我們需要涉及到非托管的資源,比如I/O操作,將緩沖區(qū)中的文本內(nèi)容保存到文件中、網(wǎng)絡(luò)通訊,發(fā)送數(shù)據(jù)包等等。
由于這些操作GC沒有辦法控制,所以也就沒有辦法來管理它們的生命周期。如果使用了非托管資源之后,沒有及時(shí)進(jìn)行釋放資源,那么就會(huì)造成內(nèi)存的泄漏問題。
而.NET為我們提供了一些手段來進(jìn)行資源釋放的操作:
析構(gòu)函數(shù)
析構(gòu)函數(shù)在C#中是一個(gè)語法糖,在構(gòu)造函數(shù)前方加一個(gè)~
符號(hào)即代表使用析構(gòu)函數(shù) 。
public class ExampleClass { public ExampleClass() { } ~ExampleClass() // 析構(gòu)函數(shù) { // 釋放非托管資源 } }
當(dāng)一個(gè)類申明了析構(gòu)函數(shù)了之后,GC將會(huì)對它進(jìn)行特殊的處理,當(dāng)該實(shí)例的資源被GC回收之前會(huì)調(diào)用析構(gòu)函數(shù)。(該部分內(nèi)容本文將不做過多介紹)
雖然析構(gòu)函數(shù)方法在某些需要進(jìn)行清理的情況下是有效的,但它有下面兩個(gè)嚴(yán)重的缺點(diǎn):
- 只有在GC檢測到某個(gè)對象可以被回收時(shí)才會(huì)調(diào)用該對象的終結(jié)方法,這發(fā)生在不再需要資源之后的某個(gè)不確定的時(shí)間。這樣一來,開發(fā)人員可以或希望釋放資源的時(shí)刻與資源實(shí)際被終結(jié)方法釋放的時(shí)刻之間會(huì)有一個(gè)延遲。如果程序需要使用許多稀缺資源(容易耗盡的資源)或不釋放資源的代價(jià)會(huì)很高(例如,大塊的非托管內(nèi)存),那么這樣的延遲可能會(huì)讓人無法接受。
- 當(dāng)CLR需要調(diào)用終結(jié)方法時(shí),它必須把回收對象內(nèi)存的工作推遲到垃圾收集的下一輪(終結(jié)方法會(huì)在兩輪垃圾收集之間運(yùn)行)。這意味著對象的內(nèi)存會(huì)在很長一段時(shí)間內(nèi)得不到釋放。
因此,如果需要盡快回收非托管資源,或者資源很稀缺,或者對性能要求極高以至于無法接受在GC時(shí)增加額外開銷,那么在這些情況下完全依靠析構(gòu)函數(shù)的方法可能不太合適。
而框架提供了IDisposable
接口,該接口為開發(fā)人員提供了一種手動(dòng)釋放非托管資源的方法,可以用來立即釋放不再需要的非托管資源。
IDisposable
從.NET Framework 1.1
開始 ,.NET就為我們提供了IDispose
接口。
使用該接口,我們可以實(shí)現(xiàn)名為Dispose
的方法,進(jìn)行一些手動(dòng)釋放資源的操作(包括托管資源和非托管資源)。
public class ExampleClass:IDisposable { private Stream _memoryStream = new MemoryStream(); public ExampleClass() { } public void Dispose() { // 釋放資源 myList.Clear(); myData = null; _memoryStream.Dispose(); } }
在C#中,我們除了可以手動(dòng)調(diào)用 xx.Dispose()
方法來觸發(fā)釋放之外,還可以使用using
的語法糖。
當(dāng)我們在 visual studio 中添加IDisposable
接口時(shí),它會(huì)提示我們使用是否使用“釋放模式”:
“釋放模式”所生成的代碼如下:
protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: 釋放托管狀態(tài)(托管對象) } // TODO: 釋放未托管的資源(未托管的對象)并重寫終結(jié)器 // TODO: 將大型字段設(shè)置為 null disposedValue = true; } } // // TODO: 僅當(dāng)“Dispose(bool disposing)”擁有用于釋放未托管資源的代碼時(shí)才替代終結(jié)器 // ~ExampleClass() // { // // 不要更改此代碼。請將清理代碼放入“Dispose(bool disposing)”方法中 // Dispose(disposing: false); // } public void Dispose() { // 不要更改此代碼。請將清理代碼放入“Dispose(bool disposing)”方法中 Dispose(disposing: true); GC.SuppressFinalize(this); }
釋放資源的代碼被放置在 Dispose(bool disposing)
方法中,你可以選用 析構(gòu)函數(shù) 或者 IDisposable
來進(jìn)行調(diào)用該方法。
這里說一下:在 IDisposable
的實(shí)現(xiàn)中,有一句 GC.SuppressFinalize(this);
。 這句話的意思是,告訴GC,不需要對該類的析構(gòu)函數(shù)進(jìn)行單獨(dú)處理了。也就是說,該類的析構(gòu)函數(shù)將不會(huì)被調(diào)用。因?yàn)橘Y源已經(jīng)在 Dispose()
中被我清理了。
異步時(shí)代
從.NET Core
開始,就意味著.NET來到了一個(gè)全新的異步時(shí)代。無論是各種基礎(chǔ)類庫(比如System.IO
)、AspNet Core、還是EFCore… 它們都支持異步操作,應(yīng)該說是推薦異步操作。
在今天,假如一個(gè)新項(xiàng)目沒有使用 await
和 async
。你都會(huì)覺得自己在寫假代碼。
現(xiàn)在越來越多的開發(fā)者都愛上了這種異步方式:不阻止線程的執(zhí)行,帶來高性能的同時(shí)還完全不需要更改原有的編碼習(xí)慣,可謂是兩全其美。
所以從.NET Core
開始到現(xiàn)在的.NET 5
,每一次版本更迭都會(huì)有一批API提供了異步的版本。
IAsyncDisposable的誕生
為了提供這樣一種機(jī)制讓使用者能夠執(zhí)行資源密集型的處置操作,而不會(huì)長期阻塞GUI應(yīng)用程序的主線程,我們讓操作成為了異步。
同樣,釋放資源的時(shí)候我們能否成為異步呢? 假如一次釋放操作會(huì)占耗費(fèi)太多的時(shí)間,那為什么我們不讓它去異步執(zhí)行呢?
為了解決這一問題,同時(shí)更好的完善.NET
異步編程的體驗(yàn),IAsyncDisposable
誕生了。
它的用法與IDisposable
非常的類似:
public class ExampleClass : IAsyncDisposable { private Stream _memoryStream = new MemoryStream(); public ExampleClass() { } public async ValueTask DisposeAsync() { await _memoryStream.DisposeAsync(); } }
當(dāng)然,using
的語法糖同樣適用于它。不過,由于它是異步編程的風(fēng)格,在使用時(shí)記得添加await
關(guān)鍵字:
await using var s = new ExampleClass() { // doing };
當(dāng)然在 C# 8
以上,我們可以使用using作用域
的簡化寫法:
await using var s = new ExampleClass(); // doing
IAsyncDisposable與IDisposable的選擇
有一個(gè)關(guān)鍵點(diǎn)是: IAsyncDisposable
其實(shí)并沒有繼承于 IDisposable
。
這就意味著,我們可以選擇兩者中的任意一個(gè),或者同時(shí)都要。
那么我們到底該選擇哪一個(gè)呢?
這個(gè)問題其實(shí)很類似于EF剛為大家提供SaveChangesAsync
方法的時(shí)候,到底我們該選用SaveChangesAsync
還是SaveChanges
呢?
在以往同步版本的代碼中,我們往往會(huì)選擇SaveChanges
同步方法。 當(dāng)來到了異步的環(huán)境,我們往往會(huì)選擇SaveChangesAsync
。
所以在AspNet Core
這個(gè)全流程異步的大環(huán)境下,我們的代碼潛移默化的就會(huì)更改為SaveChangesAsync
。
而IAsyncDisposable
也是同理的,當(dāng)我們處于異步的環(huán)境中,所使用的資源提供了異步釋放的接口,那么我們肯定就會(huì)自然而然的使用IAsyncDisposable
。
在.NET 5
之后,大部分的類都具有了IAsyncDisposable
的實(shí)現(xiàn)。比如:
Utf8JsonWriter
、StreamWriter
這些與文件操作有關(guān)的類;DbContext
這類數(shù)據(jù)庫操作類Timer
- 依賴注入的
ServiceProvider
- ………………
接下來的.NET
版本中,我們也會(huì)看到AspNet Core
中的Controller
等對于IAsyncDisposable
提供支持。
可以預(yù)測是,在未來的.NET
發(fā)展中,全異步的發(fā)展是必然的。后面越來越的已有庫會(huì)支持異步的所有操作,包括IAsyncDisposable
的使用也會(huì)越來越頻繁。
Asp Net Core 依賴注入中的IAsyncDisposable
對于咱們使用AspNet Core
的開發(fā)人員來說,我們在大多數(shù)情況下都會(huì)依賴于框架所提供的依賴注入功能。
而依賴注入框架,會(huì)在作用域釋放的時(shí)候,自動(dòng)去調(diào)用所注入服務(wù)的釋放接口IDisposable
。
比如我們把 DbContext
注入之后,其實(shí)就只管使用就行了,從來不會(huì)關(guān)心它的Dispose
問題。 相對于傳統(tǒng)using(var dbContext = new MyDbContext)
的方式要省心很多,也不會(huì)擔(dān)心忘記寫釋放而導(dǎo)致的數(shù)據(jù)庫連接未釋放的問題。
那么,當(dāng)IAsyncDisposable
出現(xiàn)之后呢?會(huì)出現(xiàn)什么情況:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddScoped<DemoDisposableObject>(); // 注入測試類 } public class DemoDisposableObject : IAsyncDisposable { public ValueTask DisposeAsync() { code here // 當(dāng)完成一次http 請求后,該方法會(huì)自動(dòng)調(diào)用 } }
當(dāng)我們實(shí)現(xiàn)了IAsyncDisposable
之后,會(huì)被自動(dòng)調(diào)用。
那么如果 IAsyncDisposable
和 IDisposable
一同使用呢?
public class DemoDisposableObject : IAsyncDisposable,IDisposable { public void Dispose() { code here } public ValueTask DisposeAsync() { code here } }
這樣的結(jié)果是:只有DisposeAsync
方法會(huì)被調(diào)用。
為什么會(huì)有這樣的結(jié)果呢? 讓我們一起來扒開它的面紗。
以下代碼位于 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(); } }
為了方便起見,我省略了部分代碼。 這里的關(guān)鍵代碼在于: DisposeAsync()
方法,它會(huì)在內(nèi)部進(jìn)行判斷,IServiceScope
是否為IAsyncDisposable
類型。如果是,則會(huì)采用它的IServiceScope
的異步釋放方法。
所以本質(zhì)上還是回到了官方依賴注入框架中IServiceScope
的實(shí)現(xiàn):
以下代碼位于 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實(shí)現(xiàn)一定是繼承了IAsyncDisposable接口
,所以在上面的AspNet Core
的代碼里,它一定會(huì)調(diào)用IServiceScope
的DisposeAsync()
方法。
而IServiceScope
的默認(rèn)實(shí)現(xiàn)在異步釋放時(shí)會(huì)進(jìn)行判斷:如果注入的實(shí)例為IAsyncDisposable
則調(diào)用DisposeAsync()
,否則判斷是否為IDisposable
。
這也解釋了為什么我們在上面同時(shí)實(shí)現(xiàn)兩個(gè)釋放接口,卻只有異步版本的會(huì)被調(diào)用。
總結(jié)
在上面的文章中,我們了解到IAsyncDisposable
作為.NET
異步發(fā)展中一個(gè)重要的新接口,在應(yīng)用上會(huì)被越來越頻繁的使用,它將逐步完善.NET
的異步生態(tài)。
當(dāng)存在下方的情況時(shí),我們應(yīng)該優(yōu)先考慮來使用它:
- 當(dāng)內(nèi)部擁有的資源具有對
IAsyncDisposable
的實(shí)現(xiàn)(比如Utf8JsonWriter
等),我們可以采用使用IAsyncDisposable
來對他們進(jìn)行釋放。 - 當(dāng)在異步的大環(huán)境下,新編寫一個(gè)需要釋放資源的類,可以優(yōu)先考慮使用
IAsyncDisposable
。
現(xiàn)在.NET
的很多類庫都已經(jīng)同時(shí)支持了IDisposable
和IAsyncDisposable
。而從使用者的角度來看,其實(shí)調(diào)用任何一個(gè)釋放方法都能夠達(dá)到釋放資源的目的。就好比DbContext
的SaveChanges
和SaveChangesAsync
。
但是從未來的發(fā)展角度來看,IAsyncDisposable
會(huì)成使用的更加頻繁。因?yàn)樗鼞?yīng)該能夠優(yōu)雅地處理托管資源,而不必?fù)?dān)心死鎖。
而對于現(xiàn)在已有代碼中實(shí)現(xiàn)了IDisposable
的類,如果想要使用IAsyncDisposable
。建議您同時(shí)實(shí)現(xiàn)兩個(gè)接口,已保證使用者在使用時(shí),無論調(diào)用哪個(gè)接口都能達(dá)到效果,而達(dá)到兼容性的目的。
類似于下方代碼:
節(jié)選自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); } }
到此這篇關(guān)于詳解C#中IAsyncDisposable接口的使用的文章就介紹到這了,更多相關(guān)C# IAsyncDisposable內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#利用Openxml讀取Excel數(shù)據(jù)實(shí)例
這篇文章主要介紹了C#利用Openxml讀取Excel數(shù)據(jù)的方法,包括使用中的注意點(diǎn)分析及疑難探討,需要的朋友可以參考下2014-09-09C#事件處理和委托event delegate實(shí)例簡述
這篇文章主要介紹了C#事件處理和委托event delegate的簡單實(shí)例,較為詳細(xì)的講述了C#事件處理和委托的聲明與實(shí)現(xiàn)過程,代碼簡單易懂,需要的朋友可以參考下2014-09-09C#使用迭代法實(shí)現(xiàn)Fibnaci數(shù)列
這篇文章主要介紹了C#使用迭代法實(shí)現(xiàn)Fibnaci數(shù)列的方法,較為詳細(xì)的分析了Fibnaci數(shù)列的原理與迭代法實(shí)現(xiàn)技巧,需要的朋友可以參考下2015-05-05C#學(xué)習(xí)筆記整理_變量等基礎(chǔ)語法(必看篇)
下面小編就為大家?guī)硪黄狢#學(xué)習(xí)筆記整理_變量等基礎(chǔ)語法(必看篇)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-09-09C#中DataTable 轉(zhuǎn)換為 Json的方法匯總(三種方法)
JavaScript Object Notation (Json)是一種輕量級(jí)的數(shù)據(jù)交換格式,下面小編給大家介紹三種方法實(shí)現(xiàn)DataTable轉(zhuǎn)換成 Json 對象,感興趣的朋友一起看看吧2016-11-11C#中WPF ListView綁定數(shù)據(jù)的實(shí)例詳解
這篇文章主要介紹了C#中WPF ListView綁定數(shù)據(jù)的實(shí)例詳解的相關(guān)資料,希望通過本文能幫助到大家,讓大家理解掌握這部分內(nèi)容,需要的朋友可以參考下2017-10-10winform多線程組件BackgroundWorker使用
這篇文章介紹了winform多線程組件BackgroundWorker的使用方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05