System.Net.HttpListener が .NET Core の時だけメモリリークっぽい現象になる件の解決策

FireScratch を C# で作っていた時に発覚したメモリーリックっぽい現象に当たったので、珍しく issue https://github.com/dotnet/corefx/issues/32454 を立てました。

サーバーのほうで、.NET Core か .NET Framework を使って以下のような簡易 HTTP サーバーを作ります。

namespace CheckHttpListener
 {
     class Program
     {
         static void Main(string[] args)
         {
             Console.WriteLine("test server .net framework");
             var listener = new System.Net.HttpListener();
             listener.Prefixes.Add("http://127.0.0.1:5411/");
             listener.Start();
             while (true)
             {
                 var context = listener.GetContext();
                 var res = context.Response;
                 res.StatusCode = 200;
                 var sw = new System.IO.StreamWriter(res.OutputStream);
                 sw.Write(string.Format("text {0}", DateTime.Now.ToString()));
                 sw.Close();
             }
         }
     }
 }

これに対して、テスト用にアクセスするクライアントを作っておきます。

namespace CheckClient
{
    class Program
     {
         static void Main(string[] args)
         {
             HttpClient client = new HttpClient();
             int cnt = 0;
             while ( true )
             {
                 var res = client.GetAsync("http://localhost:5411").Result;
                 var text = res.Content.ReadAsStringAsync().Result;
                 Console.WriteLine($"{cnt} {text}");
                 cnt++;
                 System.Threading.Thread.Sleep(20);
             }
         }
     }
 }

これを動かしておくと、.NET Framework の時にはサーバーのメモリが程よい大きさで止まるのですが、.NET Core のほうは、がんがんとメモリを食い潰してしまって最後にサーバーが倒れます。だいたい1時間ぐらい放置しておくと落ちます。

image

同じコードで、.NET Framework と .NET Core の挙動が異なるので、.NET Core の HttpListener のバグか?とも思ったのですが、どうやら HttpListener.GetContext が HttpListnerContonet が持つ Response オブジェクトの挙動が異なるので、一概にバグとは言えないような感じです。

Response を明示的に Close するか using を使う

解決策としては、Response に対して明示的に Close メソッドを呼び出すか、using を使って暗黙に Response が閉じられるようにします。

var context = listener.GetContext();
var res = context.Response;
 res.StatusCode = 200;
var sw = new System.IO.StreamWriter(res.OutputStream);
 sw.Write(string.Format("text {0}", DateTime.Now.ToString()));
 sw.Close();
 res.Close(); // ★

あるいは、

var context = listener.GetContext();
using (var res = context.Response)  // ★
 {
     res.StatusCode = 200;
     using (var sw = new System.IO.StreamWriter(res.OutputStream))
     {
         sw.Write(string.Format("text {0}", DateTime.Now.ToString()));
     }
}

のように書きます。この Close 処理は .NET Framework でも有効なので、これで同じコードで .NET Core でも .NET Framework でもメモリリークが出ないようになります。

大抵のサンプルは HttpListener をたくさん回さないので、この問題にあたることはないのですが、実際に作ってサーバー化すると数時間後に落ちたりするので注意が必要ですね。これ、Windows + .NET Core だけの現象なのか、それとも Linux + .NET Core でも発生するのかはあとで調べておきます。

カテゴリー: C# パーマリンク