4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

.net coreでプログラム終了時にデストラクタ(ファイナライザ)呼ばれない問題

Last updated at Posted at 2019-08-01

#説明

 タイトル通り、.net core向けexeのデストラクタがプログラム終了時に呼ばれない問題見つけたので・・・

 .net framework向けにビルドしたexeは最後に普通にデストラクター呼ばれるのに
.net core向けにすると、デストラクター呼ばれません

 中々気づきにくい事なのに、場合によって結構厄介な事になりそうだよねこれは・・・

 現状だと、デストラクタ使用コード書く場合は、
.net core向けにしない方がいいのかもしれません

(F#は始めたばかりで分からないことが多くて、コード例増やせない・・・)

#コード例1(C#版)

using System;

namespace Destructor {
	sealed class A {
		/*
		.net framework向けだとプログラム終了時にデストラクタが呼ばれるけど、
		.net core向けにするとプログラム終了時にデストラクタが呼ばれない・・・
		*/
		~A() { Console.WriteLine("Call Finalize A"); }
	}
	 
	static class Program {
		static A a = new A();

		static int Main(string[] args) {
			return 0;
		}
	}
}

#コード例2(C#版)

using System;
using System.IO;

namespace Destructor {
	static class Program { 
		/* 
		 static変数の場合
		
		 実行ファイルが、.net framework向けだとプログラム終了時にデストラクタが必ず呼ばれるが、
		.net core向けだと、プログラム終了前にnull代入しておかないとデストラクタが呼ばれない
		
		 null代入しないと、Static1とStatic2に代入したFileStreamデストラクタ呼ばれないのに、
		どちらかにnull代入すると、何故か両方とも呼ばれる
		*/ 
		public static FileStream Static1 = new FileStream("static1.txt", FileMode.Create);
		public static FileStream Static2 = new FileStream("static2.txt", FileMode.Create);
	
		static void Main() {
			//Test.Static3の初期化
			Test.InitStatic3();

			/*
			 null代入したので、
			プログラム終了時に、Static1とStatic2に代入したFileStreamのデストラクタが呼ばれる
			*/ 
			Static1 = null;
			/*
			 null代入したので、
			プログラム終了時に、Static3に代入したFileStreamのデストラクタが呼ばれる
			*/
			Test.Static3 = null;

			/*
			 ローカル変数の場合

			 実行ファイルが、.net framework向けでも.net core向けでも
			FileStreamのデストラクタが呼ばれる
			*/
			var local = new FileStream("local.txt", FileMode.Create);
		}
	}

	static class Test {
		/*
		.net framework向けなら、初期化後にnull代入しなくても
		Static3に代入したFileStreamのデストラクタが呼ばれる

		.net core向けの場合、プログラム終了前に事前にnull代入しておかないと
		デストラクタが呼ばれない
		*/
		public static FileStream Static3 = new FileStream("static3.txt", FileMode.Create);

		//Static3を初期化するためだけの空の関数
		public static void InitStatic3() { }
	}
}


#コード例1(F#版)


open System

type A() = class 
 (*
 .net framework向けだとプログラム終了時にデストラクタが呼ばれるけど、
 .net core向けにするとプログラム終了時にデストラクタが呼ばれない・・・
 *)
 override x.Finalize() = Console.WriteLine("Call Finalize A")
 end
  
let a : A = new A()

[<EntryPoint>]
let main argv =
  0

#代用策
.net coreからデストラクタによるオブジェクト破棄保障も取れないし、
プログラムの最後にGC.Collectを呼ぶのがなんだかって感じになったので、
ちょっとそれに代わるコード考えてました

おおよそ以下のような感じでコード書いたら、破棄漏れしないですかね?

大事な部分はコンストラクタ内でtry文内で例外送出される可能性のある処理を実装して、
例外が投げられたらcatch文内でDisposeを呼んだ後に
例外の再送出して解放漏れを防いでいるとこです

using System;

namespace TestCode {
	public static class Program {
		public static int Main(string[] args) {
			try {
				using (var root = new Root()) {

				}
			}
			catch { }

			return 0;
		}
	}

	sealed class Root : IDisposable {
		/*
		 破棄対象のオブジェクトは、フィールド宣言時に代入しないこと

		 やってしまうと、変数代入前の為、
		 例外が出た時にこのクラス内のオブジェクトが破棄されないため危険
		 */
		TestObj Obj;
		TestObj[] Ary;

		public Root() {
			/*
			 
			 try文内で例外が投げられたら、
			 catch文内でDisposeを呼んで、例外の再スローし、これを使用したオブジェクトも破棄する
			 
			*/
			try { Init(); }
			catch {
				Dispose();
				throw new Exception();
			}
		}

		void Init() {
			//try文内で破棄対象オブジェクトを生成すること
			Obj = new TestObj("obj", 0);

			/*
			 破棄対象オブジェクトを格納する為の配列は、
			 作成時と同時に配列に入れない

			 やってしまうと、変数代入前の為、
			 例外が出た時に配列内のオブジェクトが破棄されないため危険
			 */
			Ary = new TestObj[5];
			/*
			 try文内で破棄対象オブジェクトをfor文を使って配列に代入することで破棄漏れを防ぐ
			*/
			for (var i = 0; i < Ary.Length; i++) {
				Ary[i] = new TestObj("ary", i);
			}
		}

		//破棄処理中に例外を投げてしまわないように注意して破棄処理をする
		public void Dispose() {
			Console.WriteLine("Root Call Dispose");

			Obj?.Dispose();
			if (Ary != null) { foreach (var i in Ary) i?.Dispose(); }
		}
	}

	sealed class TestObj : IDisposable {
		string Name;
		int Num;
		bool IsInit;

		public TestObj(string name, int num) {
			/*
			 
			 try文内で例外が投げられたら、
			 catch文内でDisposeを呼んで、例外の再スローし、これを使用したオブジェクトも破棄する
			 
			*/
			try { Init(name, num); }
			catch {
				Dispose();
				throw new Exception();
			}
		}

		void Init(string name, int num) {
			if (IsInit) return;

			OnInit(name, num);
			IsInit = true;
		}
		public void Dispose() {
			if (!IsInit) return;

			OnDispose();
			IsInit = false;
		}

		void OnInit(string name, int num) {
			Name = name;
			Num = num;
			if (num == 3) throw new Exception();
		}

		//破棄処理中に例外を投げてしまわないように注意して破棄処理をする
		void OnDispose() {
			Console.WriteLine("TestObj Call Dispose : Name " + Name + " , Num " + Num);
		}
	}
}

#仕様の考察など

 System.Runtime.Loader.AssemblyLoadContextクラスのUnloadingイベントを使うことで、
アプリ終了時に破棄処理の実行が可能であることがわかりましたので、どうにか書いてみました

(おそらく、標準ライブラリのFileStreamクラス辺りも、
概ね下記のようにして必ず破棄処理が実行されるように実装されてるのではないかと思います)

using System;
using System.Collections.Generic;
using System.Runtime.Loader;

namespace Test {
	/*
	AssemblyLoadContextを使って、アプリ終了時に
	未開放アンマネージリソースを解放
	*/
	sealed class TestObjsAssemblyLoadContext : AssemblyLoadContext {
		public static TestObjsAssemblyLoadContext Obj { get; } = new TestObjsAssemblyLoadContext();

		//WeakReference<T>を使って、ガベージコレクションによる削除対象にする
		List<WeakReference<TestObj>> TestObjs { get; } = new List<WeakReference<TestObj>>();

		TestObjsAssemblyLoadContext() : base(false) {
			//Unloading イベントは、アプリ終了時に実行されるので、
			//これを使ってガベージコレクション発生時に削除されなかったオブジェクトが持つリソースを解放
			Unloading += (alc) => {
				foreach (var i in TestObjs) {
					if (i == null)continue;
					if (i.TryGetTarget(out var obj))obj?.OnDisposeSelf(false);
				}
			};
		}
		public void Remove(TestObj obj) {
			for (var i = 0; i < TestObjs.Count; i++) {
				TestObjs[i].TryGetTarget(out var use);

				if (use != obj)continue;

				TestObjs[i].SetTarget(null);
				break;
			}
		}
		public void Add(TestObj obj) {
			var nullIndex = -1;

			for (var i = 0; i < TestObjs.Count; i++) {
				TestObjs[i].TryGetTarget(out var use);

				if (use != null)continue;

				nullIndex = i;
				break;
			}

			if (nullIndex == -1)TestObjs.Add(new WeakReference<TestObj>(obj));
			else TestObjs[nullIndex].SetTarget(obj);
		}
	}
	sealed class TestObj : IDisposable {
		public int Num { get; }
		public TestObj(int num) {
			Num = num;
			//ここで、AssemblyLoadContext.Unloadingイベントの解放対象にする
			TestObjsAssemblyLoadContext.Obj.Add(this);
		}

		~TestObj() => OnDisposeSelf(false);
		public void Dispose() {
			OnDisposeSelf(true);
			//Dispose関数を呼んだら、ファイナライザーを呼ばないようにする
			GC.SuppressFinalize(this);
			//Dispose関数を呼んだら、AssemblyLoadContext.Unloadingイベントの解放対象から外す
			TestObjsAssemblyLoadContext.Obj.Remove(this);
		}

		internal void OnDisposeSelf(bool disposing) {
			if (disposing)System.Console.WriteLine("\tNot GC");

			System.Console.WriteLine("Call TestObj.OnDispose() , Num : " + Num);
		}
	}

	static class Program {
		static TestObj TestObj { get; set; } = new TestObj(0);
		static int Main(string[] args) {
			//このように、static変数を使わないと実体化しない様
			TestObj.ToString();

			void newTestObj(int num) => new TestObj(num);
			void newTestObjAndDispose(int num) => new TestObj(num).Dispose();

			newTestObj(1);
			newTestObjAndDispose(2);

			System.Console.WriteLine("\tRun GC.Collect And Wait");
			GC.Collect();
			System.Threading.Thread.Sleep(500);

			newTestObj(3);

			System.Console.WriteLine("\tProgram End");

			return 0;
		}
	}
}

#最低限のサンプルコード

上記コードは無駄に長いので、短くまとめたコードを下記に載せます
間違いがなければこれで合っているはずです

.net coreでのアンマネージリソース破棄処理は、
下記のように System.Runtime.Loader.AssemblyLoadContext クラスを使って実装するといいと思います

using System;
using System.Collections.Generic;
using System.Runtime.Loader;

namespace TestProgram
{
	#region ここが一番大事
	sealed class TestAssemblyLoadContext : AssemblyLoadContext
	{
		public static TestAssemblyLoadContext Obj { get; } = new TestAssemblyLoadContext();

		List<WeakReference<Test>> Tests { get; } = new List<WeakReference<Test>>();

		public TestAssemblyLoadContext() : base(false)
		{
			static void unloadingAct(AssemblyLoadContext sender)
			{
				var self = sender as TestAssemblyLoadContext;

				foreach (var i in self.Tests)
				{
					if (!i.TryGetTarget(out var obj)) continue;

					obj.DisposeSelf();
				}
			}

			Unloading += unloadingAct;
		}

		public void Add(Test add_obj)
		{
			var is_added = false;

			for (var i = 0; i < Tests.Count; i++)
			{
				var obj_haser = Tests[i];

				obj_haser.TryGetTarget(out var has_obj);

				if (has_obj == null)
				{
					obj_haser.SetTarget(add_obj);
					is_added = true;
					break;
				}
			}

			if (!is_added) Tests.Add(new WeakReference<Test>(add_obj));
		}
		public void Remove(Test remove_obj)
		{
			for (var i = 0; i < Tests.Count; i++)
			{
				var obj_haser = Tests[i];

				obj_haser.TryGetTarget(out var has_obj);

				if (has_obj == remove_obj)
				{
					Tests.RemoveAt(i);
					break;
				}
			}
		}
	}
	#endregion

	sealed class Test : IDisposable
	{
		public Test()
		{
			TestAssemblyLoadContext.Obj.Add(this);
		}

		~Test()
		{
			System.Console.WriteLine("Call ~Test()");
			DisposeSelf();
		}

		public void Dispose()
		{
			System.Console.WriteLine("Call Dispose()");
			GC.SuppressFinalize(this);
			TestAssemblyLoadContext.Obj.Remove(this);
			DisposeSelf();
		}

		internal void DisposeSelf()
		{
			System.Console.WriteLine("Call DisposeSelf()");
		}
	}

	unsafe static class Program
	{
		static int Main()
		{
			void NewTest()
			{
				new Test();
				//using var test = new Test();
			}

			System.Console.WriteLine("\t・Program Start");
			NewTest();
			//GC.Collect(); sSystem.Threading.Thread.Sleep(100);
			System.Console.WriteLine("\t・Program End");

			return 0;
		}
	}
}

#最後
良くわからない謎仕様・・・

多分これ.net core側の不具合だと思うんで、なるべく早く直して・・・
(どこに伝えればいいやら)

来年.net 5がリリースされる時に、直ってるといいね・・・

.net coreにおける、アンマネージリソース破棄処理に関するおおよそのやり方は何となく分かりましたけど、
なんだかなあって感じの仕様ですね・・・

4
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?