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); }
内存占用图表

原因分析与解决方案
OpenCV线程本地存储(TLS)缓存
cv::findChessboardCornersSB属于OpenCV标定模块,内部会为每个线程分配临时缓存(比如特征检测中间数组、优化计算缓冲区),这些缓存通过线程本地存储(TLS)管理,默认不会在线程退出时主动释放——OpenCV假设线程会被复用,以此避免重复分配的开销。测试中每个线程完成任务后直接退出,但TLS中的缓存未被清理,导致内存残留。C++内存分配器行为
线程运行时通过malloc/new分配的内存,即使调用free/delete,内存分配器(如glibc的ptmalloc)可能会将内存保留在线程专属的内存池(arena)中,不会立即归还给操作系统。线程退出时,部分大内存块可能仍留在arena中,这也是残留内存的来源之一。可释放性处理
- 显式清理OpenCV线程缓存:在每个线程退出前,调用
cv::clearThreadLocalData(),该函数会清理当前线程的OpenCV TLS缓存,释放内部临时内存。 - 复用线程而非反复创建销毁:改用线程池复用线程,缓存可被重复利用,既避免反复分配释放的开销,也不会积累残留内存。
- 强制内存分配器回收:对于glibc环境,可设置环境变量
MALLOC_TRIM_THRESHOLD_=0,让分配器释放内存后立即归还给操作系统,但会带来一定性能损耗,仅建议在测试或内存紧张场景使用。
- 显式清理OpenCV线程缓存:在每个线程退出前,调用
内容的提问来源于stack exchange,提问作者user2062604
相关产品推荐
相关产品推荐

