You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OpenCV是否为每个调用cv::findChessboardCornersSB的线程永久分配内存?

多线程调用OpenCV cv::findChessboardCornersSB 后内存残留问题

我有2500张用于相机标定的棋盘格图像,通过启动多后台线程调用cv::findChessboardCornersSB检测角点。测试1、5、10、15、20线程后发现,线程数和分析完成后的残留内存量直接相关,线程越多残留越大——比如20线程处理2500张图后残留3760MiB内存。

已确认代码无内存泄漏:持续提交更多分析任务时,内存占用会升高但不会持续增长。希望了解该现象的原因,以及是否可以释放这些残留内存。

最小复现代码

// Test controls
#define TEST_THREADS_COUNT (15)
#define TEST_IMAGES_COUNT 3000

// The structure that contains the data needed by the background thread to perform the chessboard corner finding
typedef struct {
    uint64_t uID;
    cv::Mat FrameImage;
    int nRows; int nCols;
} ChessboardCornerFindingTaskThreadData;

// The vector of tasks waiting to be completed, and the mutex that protects access to it
std::vector<ChessboardCornerFindingTaskThreadData> *g_pWaitingTasksVector = NULL;
boost::mutex g_WaitingTasksVectorMutex;

// Whether or not the test is ready to start, and whether or not it's been completed
std::atomic<bool> g_bTestReadyToStart(false);
std::atomic<bool> g_bTestComplete(false);

// The function to run in each background thread
void* TestThreadFunc(void *pArg)
{
    // Variables used in each loop iteration
    bool bHasTask = false;
    bool bLastTask = false;
    bool bChessboardFound = false;
    ChessboardCornerFindingTaskThreadData TaskToComplete;

    // While we're busy analyzing frames
    while (true)
    {
        // If the test is done
        if (g_bTestComplete.load())
        {
            // Break out of this thread's loop
            break;
        }

        // We have not yet determined that a task is available
        bHasTask = false;

        // If we're ready to start
        if (g_bTestReadyToStart.load())
        {
            // Lock the mutex that protects access to the vector of tasks
            g_WaitingTasksVectorMutex.lock();

            // If a task is available
            if ((NULL != g_pWaitingTasksVector) && (0 < g_pWaitingTasksVector->size()))
            {
                // Get a copy of the task to complete
                TaskToComplete = (*g_pWaitingTasksVector)[0];

                // Remove this one from the vector
                g_pWaitingTasksVector->erase(g_pWaitingTasksVector->begin() + 0);

                // Request minimal vector allocation
                g_pWaitingTasksVector->shrink_to_fit();

                // Set that we have a task for this loop iteration
                bHasTask = true;
            }

            // Unlock the mutex that protects access to the vector of tasks
            g_WaitingTasksVectorMutex.unlock();
        }

        // If we have a task to perform
        if (bHasTask)
        {
            // Execute the chessboard corner finding
            std::vector<cv::Point2f> FoundCorners;
            bChessboardFound = cv::findChessboardCornersSB(TaskToComplete.FrameImage, cv::Size(TaskToComplete.nCols, TaskToComplete.nRows), FoundCorners, (cv::CALIB_CB_NORMALIZE_IMAGE + cv::CALIB_CB_EXHAUSTIVE));
            
            // Release this frame's data
            TaskToComplete.FrameImage.release();

            // Get whether or not this is the last task
            bLastTask = (TaskToComplete.uID >= TEST_IMAGES_COUNT);
        }

        // Or, if we don't have a task to perform right now
        else
        {
            // Wait before checking again
            usleep(1000);
        }

        // If this is the last task
        if (bLastTask)
        {
            // Set that the test is complete so that other threads can return from their loops
            g_bTestComplete.store(true);

            // Lock the mutex that protects access to the vector of tasks
            g_WaitingTasksVectorMutex.lock();

            // If the vector is still allocated
            if (NULL != g_pWaitingTasksVector)
            {
                // Delete it
                delete g_pWaitingTasksVector;
                g_pWaitingTasksVector = NULL;
            }

            // Unlock the mutex that protects access to the vector of tasks
            g_WaitingTasksVectorMutex.unlock();

            // Break out of this thread's loop
            break;
        }
    }

    // Nothing to return here
    return NULL;
}


// Start the test
void StartTest()
{
    // Create the vector of tasks that the background threads will complete
    g_pWaitingTasksVector = new std::vector<ChessboardCornerFindingTaskThreadData>;

    // We are not yet ready to start until all tasks have been queued up
    g_bTestReadyToStart.store(false);

    // The test is not complete
    g_bTestComplete.store(false);
    
    // For each thread to launch
    for (int i = 0; i < TEST_THREADS_COUNT; ++i)
    {
        // Launch this background thread
        pthread_t ThisThread;
        pthread_create((&ThisThread), NULL, TestThreadFunc, NULL);
    }

    // An ID to assign to each task
    uint64_t uID = 0;

    // For each test image
    for (int iTest = 0; iTest < TEST_IMAGES_COUNT; ++iTest)
    {
        // Increment the task ID
        ++uID;

        // Fill in a data structure that the background thread will use to perform its analysis
        ChessboardCornerFindingTaskThreadData ThisTask;
        ThisTask.uID = uID;
        ThisTask.FrameImage = cv::imread(std::string("/media/images/TestFrame.png"), cv::IMREAD_GRAYSCALE);
        ThisTask.nRows = 50;
        ThisTask.nCols = 17;

        // Add this task to the vector
        g_WaitingTasksVectorMutex.lock();
        g_pWaitingTasksVector->push_back(ThisTask);
        g_WaitingTasksVectorMutex.unlock();
    }

    // Flag the waiting background threads that they should begin analysis
    g_bTestReadyToStart.store(true);
}

内存占用图表

内存占用图表


原因分析与解决方案

  1. OpenCV线程本地存储(TLS)缓存
    cv::findChessboardCornersSB属于OpenCV标定模块,内部会为每个线程分配临时缓存(比如特征检测中间数组、优化计算缓冲区),这些缓存通过线程本地存储(TLS)管理,默认不会在线程退出时主动释放——OpenCV假设线程会被复用,以此避免重复分配的开销。测试中每个线程完成任务后直接退出,但TLS中的缓存未被清理,导致内存残留。

  2. C++内存分配器行为
    线程运行时通过malloc/new分配的内存,即使调用free/delete,内存分配器(如glibc的ptmalloc)可能会将内存保留在线程专属的内存池(arena)中,不会立即归还给操作系统。线程退出时,部分大内存块可能仍留在arena中,这也是残留内存的来源之一。

  3. 可释放性处理

    • 显式清理OpenCV线程缓存:在每个线程退出前,调用cv::clearThreadLocalData(),该函数会清理当前线程的OpenCV TLS缓存,释放内部临时内存。
    • 复用线程而非反复创建销毁:改用线程池复用线程,缓存可被重复利用,既避免反复分配释放的开销,也不会积累残留内存。
    • 强制内存分配器回收:对于glibc环境,可设置环境变量MALLOC_TRIM_THRESHOLD_=0,让分配器释放内存后立即归还给操作系统,但会带来一定性能损耗,仅建议在测试或内存紧张场景使用。

内容的提问来源于stack exchange,提问作者user2062604

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 17:35:21