次の方法で共有


スレッド作成によるメモリリークについて

質問

2018年12月7日金曜日 8:53

お世話になっております。

VC;;2017(Intel Compilerでも)で基本的なスレッド作成APIでメモリリークが発生します。

コードが以下の通りです。

#include <iostream>
#include <Windows.h>
#include <process.h>
#include <Psapi.h>
#include <thread>

unsigned int __stdcall TestThread(void* p)
{
    return 0;
}

DWORD WINAPI ThreadFunc(LPVOID arg)
{
    return 0;
}

int main()
{
    PROCESS_MEMORY_COUNTERS pmc = {};
    DWORD dwProcessID = GetCurrentProcessId();
    HANDLE h = GetCurrentProcess();

    while (TRUE)
    {
        //int x = 0;
        //std::thread t([&] { ;;x; });
        //t.join();
        
        //HANDLE handle = CreateThread(nullptr, 0, ThreadFunc, nullptr, 0, nullptr);
        HANDLE handle = reinterpret_cast<HANDLE>(_beginthreadex(nullptr, 0, TestThread, nullptr, 0, nullptr));
        WaitForSingleObject(handle, INFINITE);
        CloseHandle(handle);

        HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessID);
        if (hProcess != nullptr)
        {
            if (GetProcessMemoryInfo(hProcess, &pmc, sizeof(pmc)))
            {
                printf("現在ワーキングセットのサイズ      :%10luバイト(%lu KB)\n", pmc.WorkingSetSize, pmc.WorkingSetSize / 1024);
            }

            CloseHandle(hProcess);
        }
    }

    return 0;
}

基本的に_beginthreadexでテストを行っていますが、コメントアウトしているCreateThreaad、std::threadでもリークします。

intel tbbのparallel_forやWin32のCreateThreadPoolで作成したスレッドではリークが起きません(ただし使用用途のため、これらのAPIを代替として使用することはできません)。

VS2017で新規でプロジェクトを作成した状態で、何もLIBファイルなどは使用していません。

何が原因かわかりますでしょうか。

よろしくお願いいたします。

すべての返信 (13)

2018年12月7日金曜日 10:22 ✅回答済み | 2 票

手元の環境で軽く実行してみましたが、リークというほどの増加傾向はみられませんでした。ループ回数を数えた上で、何回程度を見られていますか?

手元の環境で再現していない中での想像でしかありませんが、スレッドを即座に終了していることが原因かもしれません。DllMain(DLL_THREAD_ATTACH)で処理しているものの、即座に終了してしまい正しくメモリ解放できないDLLがプロセスに読み込まれているかもしれません。ウィルス対策ソフトなどを停止したら状況が変わりますか? もしくはスレッドをSleep(100)とかしてから終了したら状況は変わりますか?


2018年12月7日金曜日 9:14

まず、STLを別にするとスレッド生成関数は大まかに3つがありますが、
それぞれ特徴があります。

CreateThread     :  スレッド関数内では、C言語ラインタイムが使えない(使うとリークします)
_beginthread(ex) :  スレッド関数内では、(多くの)MFCクラスが使えない
AfxBeginThread   :  スレッド関数内で、C言語ランタイム、MFCとも使える

このあたりは万全でしょうか。


2018年12月7日金曜日 9:22

仲澤様返信ありがとうございます。

本命プロジェクトではMFCを使用していないためAfx系はテストしていません。(昔は使用していましたがリークしていたかどうかはわからず)。

CreateThreadはランタイムのリークが起こることなど把握しております、本命プロジェクトでは使用していません。

ちなみに_endthread(ex)などを使用しても同様の結果です。

上記プログラムを実行して同じ症状が出る方はいらっしゃいますでしょうか。


2018年12月7日金曜日 10:42 | 1 票

佐裕理様返信ありがとうございます。

当方もDLL_THREAD_ATTACHで増えていると思い、最小構成でテストした次第です。

増え方は1秒で500KB程度です。

(追記:Sleep(100)を挿入しても同様に増加しました。)

そして、ウィルスバスターを停止したら増えなくなりました。

解決としたいところですが、ウィルス駆除ソフトはお客様の環境でも必須のところが多く、困りました。

とりあえずウィルスバスターの設定をいろいろ触ってみます。

どうもありがとうございました。


2018年12月7日金曜日 14:24 | 1 票

誤解を与える表現がされているので補足します。

C Runtime Libraryは設計が見直されていて、static linkしていなければ、CreateThreadを使用してもリークしない設計になっています。具体的にはmsvcrXX.dllのDllMain(DLL_THREAD_DETACH)の処理で適切に解放されることと、必要なリソースは(スレッド開始時でなく)各関数が呼ばれた際に確保するように変更されているようです。
# でなければthread pool内で使えないことになってしまいます。

VC;;6.0以降のC Run-Time Library Functions for Thread Controlに以下の記述がされています。

If you are going to call C run-time routines from a program built with LIBCMT.LIB, you must start your threads with the _beginthread function. Do not use the Win32 functions ExitThread and CreateThread.

static linkの場合、dllが存在せず、DllMainも存在しないため、適切な解放処理が行えずリークします。


2018年12月7日金曜日 22:04

Win32のCreateThreadPoolで作成したスレッドではリークが起きません(ただし使用用途のため、これらのAPIを代替として使用することはできません)。

とのことですが、スレッドの生成・破棄を大量に繰り返し、なおかつそれが問題となるのであれば、やはりthread poolを導入することが順当な解決策と言えます。thread poolが使用できない要因はなんでしょうか?

また、非同期IOなどを用いることで、そもそもスレッドを生成しないアプローチもあります。ただし、thread pool以上に難易度が上がります。

ウィルス対策ソフト側で監視対象から除外する設定を行う方法もあります。ただしそのような設定を求めるアプリケーションは利用者からすれば怪しまれ避けられることも予想されます。


2018年12月9日日曜日 2:34

ゾンビ スレッドになっているのでは?
ゾンビ スレッドになると、タスク マネージャー等からはスレッドが終了しているように見えるけど、スレッド オブジェクトは解放されないので、メモリ リークが発生する。
Thread Pool で発生しないのは、ゾンビ スレッドになっていても、もともとが再利用目的でスレッドを解放しないので、あたかもリークが発生していないようにみえるだけでは?
デバッガをアタッチさせるかダンプ ファイルを取得して、ゾンビ スレッドになっていないか確認されることをお勧めします。


2018年12月9日日曜日 4:50

どうなんでしょう? ゾンビスレッドになっているなら、ハンドル数が増加するのでダンプしなくてもタスクマネージャーなどですぐに確認できます。
# 質問者さんは問題の発生する環境でハンドル数を確認してみてください。

また今回使用している_beginthreadexはsuspend状態でthreadを作成し、ハンドルを取得してからthreadを再開する仕組みですので、ゾンビスレッドにはならない設計となっています。

ちなみにVisual Studio 2015からVisual Studio でのメモリ使用のプロファイリングができます。ネイティブプログラムにおいても、どのモジュールがメモリ確保を行ったのか簡単に確認できます。これを用いて、リークを引き起こしているDLLを特定できるかもしれません。


2018年12月9日日曜日 10:12

自プロセスのハンドルとは限りません。

ウィルスバスターを停止すると現象が発生しなくなるとのことから、そのスレッドのハンドルを外部プロセスが握っている可能性が考えられます。

だとしたら、タスクマネージャーからの確認は、たぶん無理。


2018年12月10日月曜日 1:23

ご意見ありがとうございます。

自プロセスのハンドルは増加していません。

今回の件とは別と思っていたため書きませんでしたが、ウィルスバスターを起動した状態でデバッグ開始すると

> combase.dll!Microsoft::WRL::Module<1,Microsoft::WRL::Details::DefaultModule<1> >::Create() 行 1445 C;;

で例外が発生し、デバッグが開始できません。(毎回ではありません。本命プロジェクトで50%程度、本テストプログラムでほぼ100%の確率、本命プロジェクトはC#で作成C;;の各種ネイティブモジュールを呼び出し)

該当部分のコードは以下です。

C:\Program Files (x86)\Windows Kits\10\Include\10.0.17134.0\winrt\wrl\module.h

    static ModuleT& Create() throw()
    {
        static ModuleT moduleSingleton;
        return moduleSingleton;
    }

このタイプのシングルトンは2重にコンストラクタが実行されて何かしらのエラーになってると思うのですが、そもそもMicrosoftがこのコードが2重に走ることを想定して設計しないと思われます。

ウィルスバスターが何をやってるかはさっぱりわかりませんが、本命プロジェクトは24時間365日連続使用を前提としているため少量のメモリリークでも許されません。

現状で対処法はウィルスバスターの監視対象から外すしかないと思っています。

他のウィスル駆除ソフトでも同様の現象が起こるのか知りたいところです。


2018年12月10日月曜日 1:50

既に報告済みでしたら申し訳ありませんが、トレンドマイクロのウイルスバスターの窓口に報告してみてはいかがでしょうか?

どの環境でも上記のコードで現象が100%再現されるなら修正される可能性はあると思います。個人的な経験では別のベンダーですが、誤検知などの報告には親切に対応してくれる事が多いという印象です。


2018年12月10日月曜日 2:02

他のウィスル駆除ソフトでも同様の現象が起こるのか知りたいところです。

Windows DefenderとMcAfee VirusScanで試してみましたが、CreateThread / _beginthreadex / std::threadいずれも増加傾向はみられませんでした。


2018年12月10日月曜日 2:46

ウィルスバスターの設定に依存して自プロセスの挙動に変化が生じるということは、恐らくウィルスバスターのサービスが「外部」からそのプロセスにちょっかいを出しているためと考えられます。

具体的には。。。。
大抵のセキュリティソフトは、ユーザ モード プロセスの挙動を監視するために、そのプロセスに独自の DLL を Injection し API Hook を仕掛けます。
DLL Injection は当然外部プロセスから行われることになるので、その際に利用するスレッドのハンドルもその外部プロセスがオープンすることになる。
つまり、その Thread Handle は自プロセス内に保持されるわけではないので、自プロセス内の Handle Count も当然増加しない。
で、外部プロセスがその Thread Handle を保持したまま自プロセスのスレッドが終了してしまうと、そのスレッドはゾンビ化することになる。
ゾンビ化したスレッドやプロセスは、システム自体はそれらが終了されていることを認識しているので、タスクマネージャー等のツールでは終了扱いとなり、表面的には出てこない。
けど、ゾンビ化したスレッドやプロセスのオブジェクトは未解放状態なので、それらのために確保されたメモリも残り続ける。
今回のケースでは、それがリークとして見えるのでは?。。。。というのが私の推測です。

上記のような理由でプロセスやスレッドがゾンビ化した場合、そのプロセスやスレッドのハンドルを握っている外部プロセスが該当ハンドルを解放さえしてくれれば、ゾンビ化も解消されリークもなくなります。
ただ、その外部プロセスがどのようなタイミングで他のプロセス リソースを解放するかは、その外部プロセスの自由なので、そこを明確にするにはライブ デバッグ等で自プロセス内の Native API に Breakpoint を仕掛けて挙動をトレースする必要がある。
デバッガの扱いに慣れているなら、ある程度あたりをつけてトレースすることもできるけど、そーぢゃないんなら結構大変。
そもそも、今回のケースが本当にスレッドのゾンビ化によるもんなのかも確定していないので、やる価値があるかどうかもわからない。
だから、ゾンビ化の影響かどうかを確定するために、デバッガをアタッチさせるかダンプ ファイルを取得して、ゾンビ化しているかを確認することをお勧めしたわけです。
(私はライブデバッグかダンプファイルでの調査方法しか知らないのでそれを提案したのですが、それ以外にもっとお手軽な方法があるかもしれません。けど、私はそれを知らない。)