System.Threading.TimerからUIを呼び出すときにハンドルのリークを回避する方法は?
-
05-07-2019 - |
質問
System.Threading.TimerからコールバックのwinformsコントロールでInvokeを呼び出すと、タイマーが破棄されるまでハンドルがリークするようです。誰もこれを回避する方法のアイデアを持っていますか? 1秒ごとに値をポーリングし、それに応じてUIを更新する必要があります。
テストプロジェクトで試してみて、それが実際にリークの原因であることを確認しました。これは次のとおりです。
System.Threading.Timer timer;
public Form1()
{
InitializeComponent();
timer = new System.Threading.Timer(new System.Threading.TimerCallback(DoStuff), null, 0, 500);
}
void DoStuff(object o)
{
this.Invoke(new Action(() => this.Text = "hello world"));
}
これは、Windowsタスクマネージャーで見ると2ハンドル/秒でリークします。
解決
Invokeは、BeginInvoke / EndInvokeのペアのように機能し、UIスレッドにメッセージをポストし、ハンドルを作成し、そのハンドルで待機して、Invokedメソッドがいつ完了するかを判断します。 「漏れ」ているのはこのハンドルです。これらは、 Process Explorer を使用してハンドルを監視することで、名前のないイベントであることがわかります。アプリケーションの実行中。
IASyncResultがIDisposableである場合、オブジェクトの破棄はハンドルのクリーンアップを処理します。そうではないため、ガベージコレクターが実行され、IASyncResultオブジェクトのファイナライザーを呼び出すと、ハンドルがクリーンアップされます。これを確認するには、DoStuffを20回呼び出すたびにGC.Collect()を追加します。ハンドルカウントは20秒ごとに低下します。もちろん、「解決」 GC.Collect()への呼び出しを追加することによる問題は、問題に対処する間違った方法です。ガベージコレクターに独自の仕事をさせます。
Invoke呼び出しを同期する必要がない 場合は、Invokeの代わりにBeginInvokeを使用し、EndInvokeを呼び出さないでください。最終結果は同じことを行いますが、ハンドルは作成されず、「リーク」します。
他のヒント
ここでSystem.Windows.Forms.Timerを使用できない理由はありますか?タイマーがそのフォームにバインドされている場合、呼び出す必要さえありません。
さて、もう少し時間がかかりましたが、実際にはハンドルがリークしていないように見えますが、これはガベージコレクターの不確定な性質です。ティックごとに最大10ミリ秒でバンプすると、非常に速く上昇し、30秒後には低下しました。
各コールバックでGC.Collect()を手動で呼び出した理論を確認するために(実際のプロジェクトではこれをしないでください、これは単にテストするためでした、なぜそれが悪いアイデアであるかについて多くの記事があります)とハンドル数安定していました。
興味深い-これは答えではありませんが、Andreiのコメントに基づいて、これはハンドルを同じ方法でリークしないと思いましたが、OPが言及したのと同じ速度でハンドルをリークします。
System.Threading.Timer timer;
public Form2()
{
InitializeComponent();
}
private void UpdateFormTextCallback()
{
this.Text = "Hello World!";
}
private Action UpdateFormText;
private void DoStuff(object value)
{
this.Invoke(UpdateFormText);
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
timer = new System.Threading.Timer(new TimerCallback(DoStuff), null, 0, 500);
UpdateFormText = new Action(UpdateFormTextCallback);
}