【问题标题】:Why Win32 api _beginthreadex/CreateThread leaks when using CRT or not?为什么使用 CRT 时 Win32 api _beginthreadex/CreateThread 泄漏?
【发布时间】:2016-02-12 01:41:25
【问题描述】:

我有以下函数在使用 _beginthreadex 或 CreateThread 创建的线程上执行:

static volatile LONG g_executedThreads = 0;
void executeThread(int v){
   //1. leaks: time_t tt = _time64(NULL);
   //2. leaks: FILETIME ft; GetSystemTimeAsFileTime(&ft);
   //3. no leak: SYSTEMTIME stm; GetSystemTime(&stm);

   InterlockedAdd(&g_executedThreads, 1); // count nr of executions
 }

当我取消注释任何行 1.(crt 调用)或 2.(win 32 api 调用)时,线程泄漏并且 _begintreadex 的下一次调用将失败(GetLastError -> 返回错误 (8) -> 没有足够的存储空间来处理这个命令)。 Process Explorer 报告的内存,当 _beginthreadex 开始失败时: 私有 130 Mb,虚拟 150 Mb

但如果我只取消注释 3. 行(其他 win 32 api 调用),则不会发生泄漏,并且在 100 万个线程后不会失败。这里报告的内存是私有 1.4 Mb,虚拟 25 Mb。而且这个版本运行得非常快(100 万个线程需要 20 秒,而第一个需要 60 秒才能运行 30000 个线程)。

我已经使用 Visual Studio 2013 测试 (see here the test code),编译 x86(调试和发布)并在 Win 8.1 x64 上运行;创建 30000 个线程后,_beginthreadex 开始失败(大多数调用);我想提一下,同时运行的线程在 100 以下。

更新 2

我对最多 100 个线程的假设是基于控制台输出(计划与完成大致相等)并且线程选项卡中的 Process Explorer 没有报告超过 10 个线程_) 这是控制台输出(没有 WaitForSingleObject,原始代码):

step:0, scheduled:1, completed:1
step:5000, scheduled:5001, completed:5000
...
step:25000, scheduled:25001, completed:24999
step:30000, scheduled:30001, completed:30001
 _beginthreadex failed. err(8); errno(12). exiting ...
step:31701, scheduled:31712, completed:31710

rerun loop:
step:0, scheduled:31713, completed:31711
_beginthreadex failed. err(8); errno(12). exiting ...
step:6, scheduled:31719, completed:31716

根据@SHR 和@HarryJohnston 的建议,我一次安排了64 个线程,并等待全部完成,(see updated code here),但行为是相同的。注意我曾经尝试过单线程,但失败是零星的。保留堆栈大小也是 64K! 这是新的日程安排功能:

static unsigned int __stdcall _beginthreadex_wrapper(void *arg) {
    executeThread(1);
    return 0;
}
const int maxThreadsCount = MAXIMUM_WAIT_OBJECTS;
bool _beginthreadex_factory(int& step) {
    DWORD lastError = 0;

    HANDLE threads[maxThreadsCount];
    int threadsCount = 0;
    while (threadsCount < maxThreadsCount){
        unsigned int id;
        threads[threadsCount] = (HANDLE)_beginthreadex(NULL,
            64 * 1024, _beginthreadex_wrapper, NULL, STACK_SIZE_PARAM_IS_A_RESERVATION, &id);
        if (threads[threadsCount] == NULL) {
            lastError = GetLastError();
            break;
        }
        else threadsCount++;
    }

    if (threadsCount > 0) {
        WaitForMultipleObjects(threadsCount, threads, TRUE, INFINITE);
        for (int i = 0; i < threadsCount; i++) CloseHandle(threads[i]);
    }

    step += threadsCount;
    g_scheduledThreads += threadsCount;

    if (threadsCount < maxThreadsCount) {
        printf("    %03d sec: step:%d, _beginthreadex failed. err(%d); errno(%d). exiting ...\n", getLogTime(), step, lastError, errno);
        return false;
    }
    else return true;
}

这是控制台上打印的内容:

000 sec: step:6400, scheduled:6400, completed:6400
003 sec: step:12800, scheduled:12800, completed:12800
007 sec: step:19200, scheduled:19200, completed:19200
014 sec: step:25600, scheduled:25600, completed:25600
022 sec: step:32000, scheduled:32000, completed:32000
023 sec: step:32358, _beginthreadex failed. err(8); errno(12). exiting ...
sleep 5 seconds
028 sec: step:32358, scheduled:32358, completed:32358
try to create 2 more times
028 sec: step:32361, _beginthreadex failed. err(8); errno(12). exiting ...
032 sec: step:32361, scheduled:32361, completed:32361
rerun loop: 1
036 sec: step:3, _beginthreadex failed. err(8); errno(12). exiting ...
sleep 5 seconds
041 sec: step:3, scheduled:32364, completed:32364
try to create 2 more times
041 sec: step:5, _beginthreadex failed. err(8); errno(12). exiting ...
045 sec: step:5, scheduled:32366, completed:32366
rerun loop: 2
056 sec: step:2, _beginthreadex failed. err(8); errno(12). exiting ...
sleep 5 seconds
061 sec: step:2, scheduled:32368, completed:32368
try to create 2 more times
061 sec: step:4, _beginthreadex failed. err(8); errno(12). exiting ...
065 sec: step:4, scheduled:32370, completed:32370

欢迎任何建议/信息。 谢谢。

【问题讨论】:

  • 我不是 Windows 专家,但问题是几乎可以肯定在您未显示的代码中。我看到您已经发布了完整测试程序的链接,但您是否介意首先将该程序缩减为仍然存在内存泄漏的最小可能程序 ,其次,将其编辑到问题中? (除非您在削减过程中发现问题,这会发生。)
  • 如果InterlockedAdd 函数可以以任何方式终止线程,那么这可能会导致一些内存泄漏。
  • 它不是 Win32 API。这是一个C library function
  • SHR 是对的。这不是泄漏,您只需要限制线程创建以限制同时运行的线程数。 (您断言一次运行的线程不超过 100 个,但我看不出有任何方法可以证实这一点。)
  • @HarryJohnston 我的断言基于控制台上的打印内容。计划值和完成值差别不大。

标签: c++ multithreading memory-leaks


【解决方案1】:

我猜你弄错了。 看看这段代码:

int thread_func(void* p)
{
     Sleep(1000);
     return 0;
}
int main()
{
    LPTHREAD_START_ROUTINE s = (LPTHREAD_START_ROUTINE)&thread_func;
    for(int i=0;i<1000000;i++)
    {
        DWORD id;
        HANDLE h = CreateThread(NULL,0, s,NULL,0,&id); 
        WaitForSingleObject(h,INFINITE);
    }
    return 0;
}

泄漏线程会因为您调用它而泄漏,因此等待不会改变任何事情,但是当您在性能监视器中查看它时,您会看到所有行几乎都是不变的。

现在问问自己,当我删除 WaitForSingleObject 后会发生什么?

线程的创建比线程运行得快得多,因此您达到了每个进程的线程限制或每个进程的内存限制。 请注意,如果您正在为 x86 编译,内存限制为 4GB,但只有 2GB 用于用户模式内存,另外 2GB 用于内核模式内存。如果您使用线程的默认堆栈大小(1MB),并且程序的其余部分根本不使用内存(这永远不会发生,因为您有代码......),那么您将被限制为 2000 个线程。在 2GB 完成后,您无法创建更多线程,直到之前的线程结束。

所以,我的结论是您创建线程并且不要等待,并且在一段时间后,没有内存可用于更多线程。

您可以使用性能监视器检查是否属于这种情况,并检查每个进程的最大线程数。

【讨论】:

  • 查看我的更新。你的代码和我的代码之间的最大区别是我的线程函数执行得很快......如前所述(Process Explorer 中的最大步数低于 10,峰值私有字节:131Mb)
  • 另外,在第一个版本中(没有等待线程停止)我可以安排超过 30000 个线程(每个线程都有保留的堆栈 640K)而不会失败..使用你的假设 30000 * 640K ~ 30 * 640M = 18Gb,这在 x86 上没有发生(最大 2Gb 用户空间)。根据我的观察(峰值内存 = 130Mb)运行线程的最大 nr = 200(130 MB / 640K)。
  • 如果你不是在等待线程,你也应该关闭线程句柄,这表明你不关心它的退出状态,或者等待它,所以内核不会保存任何东西线程退出后给你。
  • 据我所知,原始代码和更新代码都正确关闭了句柄。
【解决方案2】:

卸载杀毒后无法重现故障(即使代码运行速度与其他场景3.一样快)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-24
    • 2015-07-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多