2013年9月29日日曜日

[.NET] WebBrowserをDisposeしても、メモリが開放されない件について

そもそも、WebBrowserがメモリリークを起こす件は、随分前からいわれ続けている気がしますが、未だに解決されていないようです。
とりあえず、試しにWebBrowserを貼り付けたフォームを作って、適当なコンテンツを表示するようにしたものを、アプリケーションのメインウィンドウから、Show()で表示させて、そのフォームを閉じるようにしてみましたが、やはりリークが発生しているようです。

いろいろ検索したところによると、Win32APIのSetProcessWorkingSetSize()やEmptyWorkingSet()の利用が効果があるとのことですが、どうやら、これらも機能しないようです。ただ、OSやIEのバージョン、WebBrowserに表示した中味などの条件で機能することもあるようです。
この手の検索をしても、かなりの量がヒットしているところを見ると、やはり、今現在でも決定的な解決方法はないのでは、と思っています。

何がメモリに留まり続けているかというのを調べてみたのですが、パフォーマンスモニタで見てみると、.NET CLR Interopの# of CCWsという項目が、増え続けています。これによって、Bytes in all HeapsやTotal committed Bytesなども増え続けているように見えます。CCWというのは、COMのラッパーのことでなので、CLRから見るとCOMが開放されない状態になっているのではないかと思われます。
WebBrowserがIEの機能をCOMとして利用しているというのは、容易に想像できますが、具体的に何をどう使っているのかはわかりません(本気で追求すればわかりかもしれませんが、とりあえずなので、すみませんw)。
この現象からすると、根本的解決を試みるのであれば、開放されないCOMに対して何か操作するということぐらいしか思いつきません。

そういう状況から考えると、WebBrowserはDisposeするような使い方はしないというのが、無難な解決方法(というか、回避方法)ではないかと、思います。例えば、上記の書いたようなフォームにWebBrowserを貼り付けて表示するのであれば、Show()で表示するのではなく、別プロセスでフォームを表示させるという方法です。プロセスが終了すれば、無条件にメモリは開放されるので、これならリークしようがないはずです。
もし、メインのウィンドウとなんだかの情報のやり取りが必要であれば、IPCが使えます(と、あっさりいいましたが、IPCも曲者でタイムアウトしたり、やたらに遅かったりする場合がある)。

というわけで、もし、WebBrowserのメモリリークでお悩みの方は、どちらかというと残念な解決方法ですが、上記の方法を検討してみたらいかがでしょうか。

2013年2月18日月曜日

[.NET] TcpListenerのよさげな使い方

小ネタですが、TcpListenerのおそらくよさげな使い方についてです。ポイントは、TcpListenerはAccept要求をキューに溜め込む仕様のため、Acceptの処理をすばやく実行しないと、新しい要求が失敗する可能性があるということです。

とりあえずコード(C#)

// TcpListenerの使用例
// クライアントとのデータのやり取りはここでやる(Thread)
void ConnectionProc(object target)
{
    TcpClient tcpClient = (TcpClient)target;
    // ここでtcpClientからストリームを取得してReadとかWriteとかCloseとかを実行する
    // ちなみにクライアント側から接続が切断されると、Readでサイズ0が返ってくる
}
// 接続を受け入れる。
void AcceptConnection(TcpListener tcpListener)
{
    TcpClient tcpClient = tcpListener.AcceptTcpClient();
    Thread thread = new Thread(ConnectionProc);
    thread.IsBackground = true;
    thread.Start(tcpClient);
}
// Acceptを受け入れる処理(Thread)
void AcceptProc(object target)
{
    TcpListener tcpListener = (TcpListener)target;
    while (true)
    {
        try
        {
            while (tcpListener.Pending())
            {
                AcceptConnection(tcpListener);
            }
            AcceptConnection(tcpListener);
        }
        catch (Exception)
        {
            // TcpListenter.Stop()が呼び出されるとSocketExceptionがThrowされる
            break;
        }
    }
}

//  Acceptを開始する処理
// tcpListenerはどこかで作成されているとする
tcpListener.Start();
Thread thread = new Thread(AcceptProc);
thread.IsBackground = true;
thread.Start(tcpListener);

正直言ってwhile (tcpListener.Pending()){…}のコードはロジック的にはなくても同じに思えるのですが、どうやらこのように書いておくと、Acceptを処理しているときに直ぐに別な接続が来た場合、早く応答するようです。
もし、TcpListenerを使っていて、クライアントからの接続に失敗するようだったら、このような方法を試してみるのがいいかもしれません。