下記の通りVB.net2008で非同期でpingを行い、結果をpostmessageで送信するDLLを作成し、VB6で定期的に呼び出し、使用しています。
しかし、VB6側でDLLを呼ぶたびにメモリ使用量がどんどん増えていき困っています。
DLL内でなんらかの開放が抜けていると思うのですがアドバイスをお願いします。
以下VB6呼び出しソース(参照設定済み)
Private Sub Timer_NWCHK_Timer()
Text3.Text = "" '応答内容を初期化
On Error GoTo Err_Proc
Dim obj As Netchk.Netchk ' (a)
Dim ret As Long
Set obj = New Netchk.Netchk ' (b)
If NWCHKPATH <> "" Then
ret = obj.ping(NWCHKPATH, Me.hWnd) ' (c)'DMZに対してping送信 レスポンスは後程DLLがSendMassageで非同期でtext3に入れてくれる
End If
Set obj = Nothing
Err_Proc:
End Sub
↓以下VB.netソース
Option Strict On
Public Class Netchk
Public Declare Function RegisterWindowMessage Lib "user32.dll" _
Alias "RegisterWindowMessageA" (ByVal lpString As String) As Integer
Declare Function PostMessageStr Lib "user32.dll" Alias "PostMessageA" _
(ByVal hwnd As Integer, ByVal MSG As Integer, _
ByVal wParam As Integer, ByVal lParam As Integer) As Integer
'Pingオブジェクト
Dim mainPing As System.Net.NetworkInformation.Ping = Nothing
Public Val_hd As Integer
Public WM_USER_PING As Integer
Public ReadOnly Property Ping(ByVal add As String, ByVal hd As Integer) As Boolean
Get
'後にPostMessageで使用するメッセージ値はRegisterWindowMessageで取得しておく
WM_USER_PING = RegisterWindowMessage("WM_USER_PING")
Val_hd = hd
'Pingオブジェクトの作成
If mainPing Is Nothing Then
mainPing = New System.Net.NetworkInformation.Ping()
'イベントハンドラを追加
AddHandler mainPing.PingCompleted, AddressOf Ping_PingCompleted
End If
'Pingのオプションを設定
'TTLを64、フラグメンテーションを無効にする
Dim opts As New System.Net.NetworkInformation.PingOptions(64, True)
'Pingで送信する32バイトのデータを作成
Dim bs As Byte() = System.Text.Encoding.ASCII.GetBytes(New String("A"c, 32))
'タイムアウトを10秒
mainPing.SendAsync(add, 10000, bs, opts, Nothing)
opts = Nothing
bs = Nothing
mainPing = Nothing
Return True
End Get
End Property
Private Sub Ping_PingCompleted(ByVal sender As Object, _
ByVal e As System.Net.NetworkInformation.PingCompletedEventArgs)
Dim Ret As Integer
If e.Cancelled Then
Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 2)
ElseIf Not (e.Error Is Nothing) Then
Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 1)
Else
'結果を取得
If e.Reply.Status = System.Net.NetworkInformation.IPStatus.Success Then
Console.WriteLine("Reply from {0}:bytes={1} time={2}ms TTL={3}", _
e.Reply.Address, e.Reply.Buffer.Length, _
e.Reply.RoundtripTime, e.Reply.Options.Ttl)
Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 7)
Else
Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 3)
End If
End If
End Sub
End Class
ActiveX DLL としての運用ですね。
VB6 連携との部分は抜きにして、まずは .NET 側のコードだけの判断ですが
Ping クラスを Dispose している箇所が無いのが気になりますね。
Ping クラスは IDisposable を実装しているはずなので…。
アドバイスありがとうございます
mainPing = Nothing
の一行前に
mainPing.dispose
を入れても変わらずでした
結果の内容によらず呼び出す度に80kくらいのメモリが食われます
System.GC.Collct
もダメなようです
VB6 側のコードは、
Set obj = New Netchk.Netchk
If NWCHKPATH <> "" Then
ret = obj.ping(NWCHKPATH, Me.hWnd)
End If
Set obj = Nothing
ではなく、
If NWCHKPATH <> "" Then
Set obj = New Netchk.Netchk
ret = obj.ping(NWCHKPATH, Me.hWnd)
Set obj = Nothing
End If
の方が良いのではないでしょうか。
また、コメントが「SendMassageで非同期でtext3に入れてくれる」とありますが、
実際に使われているのは PostMessage でしたよね。
> 呼び出す度に80kくらいのメモリが食われます
ある程度のところで打ち止めになったりはせず、80KB ずつ上限無しに消費されるのでしょうか。
.NET 側の GC の動作を見るのであれば、パフォーマンスモニタで、.NET CLR Memory の遷移を確認してみてください。
http://msdn.microsoft.com/ja-jp/magazine/dd882521.aspx
http://msdn.microsoft.com/ja-jp/library/dd297765.aspx
> mainPing.dispose
解決するかどうかは分かりませんが、.NET Framework 2.0〜3.5 では、
DirectCast(mainPing, System.IDisposable).Dispose()
で呼び出してみてください。
(.NET 4 以降であれば mainPing.Dispose のままで OK)
回答ありがとうございます。
DirectCastはどこにいれるのが正解でしょうか?
Disposeの代わりに下記の通り追加してみました。
するとPing_PingCompleted関数はe.Cancelledとなり、キャンセルしたことになりPingが成功しなくなりました。
逆にPingCompletedの最後にDirectCastの処理を入れるとVB6側のソフトが落ちました。
'タイムアウトを10秒
mainPing.SendAsync(add, 10000, bs, opts, Nothing)
DirectCast(mainPing, System.IDisposable).Dispose()
mainPing = Nothing
opts = Nothing
bs = Nothing
元のソフトでは80KB ずつ上限無しに消費します。
1週間起動していたら落ちるまではいかないものの、大量のメモリを消費したままでした。
NET CLR Memory の遷移については時間をとって調べてみます
私のイメージではVB6側で
Nothingをした時点でメモリは開放されるものと思っていたのですがなかなかうまくいかないですね。
> メモリは開放されるものと
開放は、自身が管理している資源を、他者にも利用できるよう開け放つ(free/open)こと。
解放は、自身が管理していた資源を解き放ち、自身の管理権を破棄する(dispose)こと。
学校の図書館を一般カイホウするのは前者、捕虜をカイホウするのは後者ですが、
メモリについてはどちらの意味でも使われるので、使い分けに注意が必要ですね。
閑話休題。
まず、シロさんの方で、問題個所の切り分けは出来ていますか? たとえば、
(1) COM 化することなく、単に Ping.SendAsync を繰り返した場合
(2) 何もしない DLL を作り、それをVB6から繰り返し呼び出した場合
(3) Ping クラスを生成するだけで、SendAsnyc を呼ばなかった場合
(4) WM_USER_PING メッセージ交換のみで Ping を実行しない DLL とした場合
などといった判定作業です。(他にも検討すべき個所はあるかもしれません)
先の私の回答は、上記(1)に対するものだけであり、それ以外の
要因には触れていません。もしも複合的な要因であった場合、
IDisposable.Dispose だけでは解決しない可能性があります。
さて、そもそもの Ping.SendAsnyc のメモリリーク問題については、
下記を参照してみてください。
http://blog.mbcharbonneau.com/2006/11/14/using-the-ping-class-in-net-20-without-memory-leaks/
http://blogs.msdn.com/b/joncole/archive/2005/12/15/debugging-a-memory-leak-in-managed-code_3a00_-ping-_2d00_-sendasync.aspx
.NET 2.0〜3.5 の Ping クラスは、Component クラスを継承しているのですが、
Dispose(Boolean)がオーバーライドされていないため、Ping.Dispose() を呼び出しても、
単にベースクラスの Component.Dispose() が呼ばれるだけとなり、Ping クラスの
内部リソースには影響を与えないという問題があります。そもため VB2008 から
意図的に「IDisposable の」Dispose を呼び出す必要があるということです。
なお、.NET 4 以降の Ping クラスは、Component.Dispose(Boolean) メソッドが
オーバーライドされているため、Ping.Dispose() メソッドの呼び出しだけで
問題ないと思います。個人的な予想であって、検証したわけでは無いですけれどね。
> DirectCastはどこにいれるのが正解でしょうか?
IDisposable.Dispose すべきは、オブジェクトの使用後です。
つまり、少なくとも Ping 完了後に実施されるようにしておいてください。
作成された DLL は イベントを公開していないようなので、
PingCompleted イベントを抜けた後のタイミングが適切では無いでしょうか。
> Disposeの代わりに下記の通り追加してみました。
SendAsnyc 処理中に破棄しては、流石にマズイでしょう。
> 逆にPingCompletedの最後にDirectCastの処理を入れるとVB6側のソフトが落ちました。
それは、実行時にトラップ可能なエラーでしょうか。また、どの行で落ちたのでしょうか。
現時点の情報だけでは、VB6 に何が起きたのかを私には想像できないのですが、とりあえず
(案1) VB6 側が、受信完了後に Dispose 発行依頼のメソッドを呼び出す形に修正する。
(案2) Timer 等を併用し、PingCompleted イベント完了後に Dispose する形にする。
などと修正してみるのは如何でしょうか。
その上で、DLL 側(Netchk クラス)を IDisposable パターンで
実装しておく必要もあるでしょう。理由については後述します。
http://msdn.microsoft.com/ja-jp/library/s9bwddyx%28v=vs.80%29.aspx
> Nothingをした時点でメモリは開放されるものと思っていたのですがなかなかうまくいかないですね。
そもそも、VB6 側での Nothing 代入は不要です。
今回のコードでは obj がローカル変数となっているため、何もせずとも
変数がスコープ外になった時点で、自動的に参照カウントが減じられます。
また、Nothing 代入も End Sub の直前にあるのみなので、早期解放の
意図ともなっておらず、この時点での Nothing 代入自体にはあまり意味がありません。
それはさておき、VB6 側参照カウントがゼロになった時点で、
ActiveX オブジェクトの終了処理は完了します。ただし、
ここで破棄されるのは COM が使用しているリソースだけです。
もちろんそこから付随して、COM 呼び出し可能ラッパー(CCW)によって、
.NET 側のマネージ オブジェクトの参照も自動的に解放されます。
ただしそれは、あくまでも「マネージオブジェクト」に対してのみであり、
Ping クラスのようにアンマネージなオブジェクトの場合、解放処理は
開発者自身が手動で破棄せねばなりません。
とはいえ多くのマネージオブジェクトは、プログラマが
Dispose 等の終了処理を呼び出し忘れていたとしても、GC 回収時に
終了処理が自動的に施され、アンマネージリソース等(今回の場合で言えば、
IcmpCreateFile API のハンドルなど)のリークが起きないよう
設計されています。(たとえば IcmpCloseHandle 等を呼び出すなど)
ところが .NET 3.5 以下では、先述した理由によって、Ping.Dispose が
実質機能していないため、IDisposable.Dispose の明示的実行が
必要となります。
しかも、Component が正しく実装されていませんし、かといって
Ping クラスに Finalize が明示実装されているわけでもありません。
そのため、GC 回収時にアンマネージリソースの後始末が自動的に
実施されることも無さそうなので、Ping を呼び出す自作クラス自体も、
IDisposable インターフェイスを実装するなどの対策が必要でしょう。
何度もご指導ありがとうございます。
>(1) COM 化することなく、単に Ping.SendAsync を繰り返した場合
DLLではなく.netアプリとして行うということでしょうか?
それであればメモリリークするようです
>(2) 何もしない DLL を作り、それをVB6から繰り返し呼び出した場合
メモリリークはありませんでした。
>(3) Ping クラスを生成するだけで、SendAsnyc を呼ばなかった場合
メモリリークはありませんでした。
>(4) WM_USER_PING メッセージ交換のみで Ping を実行しない DLL とした場合
メモリリークはありませんでした。
>作成された DLL は イベントを公開していないようなので、
これの意味がわかりませんでした。
公開とはどのようなことですか?
なにか公開するとメリットがあったり・公開すると別の対策や方法が生まれてくるという方法があるのでしょうか?
>PingCompleted イベントを抜けた後のタイミング
ということなのでDLL側は下記の通りしてみました。
****当初のソース内Ping関数一部抜粋*****
<★前半省略★>
'Pingで送信する32バイトのデータを作成
Dim bs As Byte() = System.Text.Encoding.ASCII.GetBytes(New String("A"c, 32))
'タイムアウトを10秒
mainPing.SendAsync(add, 10000, bs, opts, Nothing)
opts = Nothing
bs = Nothing
Return True
End Get
End Property
Private Sub Ping_PingCompleted(ByVal sender As Object, _
ByVal e As System.Net.NetworkInformation.PingCompletedEventArgs)
Dim Ret As Integer
If e.Cancelled Then
Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 2)
ElseIf Not (e.Error Is Nothing) Then
Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 1)
Else
'結果を取得
If e.Reply.Status = System.Net.NetworkInformation.IPStatus.Success Then
Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 7)
Else
Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 3)
End If
End If
DirectCast(mainPing, System.IDisposable).Dispose() ←★追加しました
End Sub
VB6側をもっとシンプルに
Private Sub Timer1_Timer()
On Error GoTo Err_Proc
Dim obj As Netchk.Netchk
Dim ret As Long
Set obj = New Netchk.Netchk
ret = obj.ping("192.168.0.1", Me.hWnd)
Err_Proc:
End Sub
のみのシンプルなフォームを作り試験をしてみました。(postmassege受信処理は作っていません)
VB6側、DLL側いかがでしょうか?
DirectCastのおかげか80kのメモリ浪費はなくなりました。落ちることもないのでその件はまた別の問題にようでした。
いまこのシンプルな試験フォームではタイマーイベント毎に8k増加しつづけているようです。
これが普通のことなのかどうかもわかりませんし時間が経過していないためどこかで上限以上消費しないのか無尽蔵に消費するのかはまだわかりません。
あとはVB2010にすると解消する等のことも考えられますか?
> (案1) VB6 側が、受信完了後に Dispose 発行依頼のメソッドを呼び出す形に修正する。
こちらの方法ですと万が一受信できないことがあるたびにDisposeができないと思うのですが大丈夫なのでしょうか?
> (案2) Timer 等を併用し、PingCompleted イベント完了後に Dispose する形にする。
別のタイマー等でDisposeを呼び出す場合、今回のコードでは obj がローカル変数となっているため別の関数でまたDisposeするためだけにset obj〜としてもよいものなのでしょうか?
> DLLではなく.netアプリとして行うということでしょうか?
> それであればメモリリークするようです
問題箇所は Ping.SendAsnyc であるという仮定で間違い無さそうですね。
> 公開とはどのようなことですか?
VB6 側で WithEvents して使うタイプの DLL ではない、ということです。
今回はイベント通知ではなく、PostMessage で通知しているのですよね。
> DirectCastのおかげか
DirectCast は単に型変換(正確にはキャスト)を行うだけのものなので
その言い方は語弊がありますが、IDisposable.Dispose の呼び出しが
必須であるという点は間違いないかと。
> 万が一受信できないことがあるたびにDisposeができないと思うのですが大丈夫なのでしょうか?
大丈夫ではないので、Netchk クラス自体を Finalize で対処して
備えておく必要があります。いわゆる IDisposable パターンの実装ですね。
> 今回のコードでは obj がローカル変数となっているため
そのコードだと、VB6 側のオブジェクト寿命管理に問題がありませんか?
それと 案2 の Timer 案において、タイマーを用意するのは DLL 側です。
VB6 の Timer コントロールのことではありません(説明不足でしたね…)。
PingCompleted 内で IDisposable.Dispose してはまずいのであれば、
PingCompleted 後に Dispose するために、System.Threading.Timer 等で
遅延処理させるというのが案2です。ただし PingCompleted 内で解放しても
先のエラー問題が再現しないのなら、その必要は無さそうですけれども。
> 8k増加しつづけているようです。
その実装(VB6 側で Timer を使っているサンプル)は、
そもそも別の問題があるので除外して…。
当初のコードにおいて、IDisposable.Dispose 以外の解放処理を
必要とするとなれば、あと思いつくのは、PingCompleted 完了時に
RemoveHandler を呼び出してイベントを解除することと、
フィールド変数である mainPing に Nothing を代入して、
GC の回収対象にされやすくすることぐらいですかね。
ただ、これらは通常気にする必要は無いと思うので、
IDisposable.Dispose のような明確な効果は得られないかもしれません。
> あとはVB2010にすると解消する等のことも考えられますか?
気にするべきは VB のバージョンではなく .NET Framework のバージョンです。
既に何度か書いたように、「.NET 4 や .NET 4.5 版の Ping クラス」では
Dispose が適切に実装されているため、IDisposable.Dispose であろうと
Ping.Dispose であろうと、どちらの呼び方でも問題ないと思われます。
また、万一 Dispose の呼び出しを忘れても Finalize によって
GC 回収時に自動破棄されることが期待されます。
なので、VB2012 や VB2010 を使えば、問題点を解消できる可能性は
ありますが、これらのバージョンでも、ターゲットフレームワークに
.NET 2.0〜3.5 を採用した場合はやはり IDisposable.Dispose を
明示的に呼び出すという、同様の対策が要求されることになるでしょう。
いろいろ試行錯誤の結果VB2010(.NET4)を使用し、
Ping_PingCompleted関数内で
MainPing.Dispose()
を行うことで全てうまくいきました。
.net3.5以前にはそのような症状があるとは知りませんでした。
大変助かりました。
ありがとうございました
ツイート | ![]() |