C++多线程磁盘扫描出现访问违规错误求助
尝试用多线程扫描系统磁盘并构建存储所有文件路径的Trie树,但测试中出现异常:在main.cpp的循环里,线程数i为2、4、6时程序运行正常,但i为8或10时(偶尔i=8能正常运行),程序会在WorkStealQueue::pop函数的if (m_tasks.empty()) return false;行触发访问违规错误,错误信息为“Access violation reading location 0x00000014”。
已尝试的排查动作:
- 检查
WorkStealQueue类的pop和push方法实现 - 调整原子变量
m_taskCount的内存序 - 改进
WorkStealThreadPool的线程同步与任务终止逻辑
均未解决问题。
问题分析
访问违规错误0x00000014表明程序在访问无效指针(偏移0x14意味着基指针为nullptr)。定位到WorkStealQueue::pop中的m_tasks.empty(),说明WorkStealQueue对象的内存已被破坏或对象已失效,而线程仍在访问它。
核心原因在于**WorkStealThreadPool::addTask函数依赖未初始化的线程本地变量**:
WorkStealThreadPool::m_index是线程本地变量,仅在worker线程启动时被赋值为有效队列索引- 主线程调用
addTask时,自身的m_index副本未被初始化,值为未定义的随机值 - 当随机值超出
m_taskQueues的有效索引范围时,会触发数组越界访问,破坏内存结构,最终导致worker线程访问队列时出现访问违规
线程数越大,越容易触发该问题:因为主线程m_index的随机值超出队列数量范围的概率随队列数量增加而变化,当队列数量为8/10时,随机值更易命中无效索引,破坏内存。
修复方案
1. 初始化线程本地变量
显式初始化线程本地变量m_index为无效值,用于区分worker线程和外部线程:
在workstealthreadpool.cpp中修改初始化代码:
#include <limits> thread_local std::size_t WorkStealThreadPool::m_index = std::numeric_limits<std::size_t>::max();
2. 修改addTask函数,安全选择任务队列
修改addTask逻辑,判断当前线程是否为worker线程:
- 如果是worker线程,使用自身的任务队列
- 如果是外部线程(如主线程),通过轮询方式选择任务队列,避免越界访问
首先在WorkStealThreadPool类中添加一个原子变量用于轮询计数:
在workstealthreadpool.h的private段添加:
std::atomic<std::size_t> m_nextQueueIndex{0};
然后修改addTask模板函数:
template<typename Callback, typename... Args> auto WorkStealThreadPool::addTask(Callback&& func, Args&&... args) -> std::future<typename std::result_of<Callback(Args...)>::type> { using returnType = typename std::result_of<Callback(Args...)>::type; auto task = std::make_shared<std::packaged_task<returnType()>>(std::bind(std::forward<Callback>(func), std::forward<Args>(args)...)); std::future<returnType> result = task->get_future(); std::size_t queueIndex; // 判断当前线程是否为worker线程(m_index在有效范围内) if (m_index < m_taskQueues.size()) { queueIndex = m_index; } else { // 外部线程轮询选择队列,避免越界 queueIndex = m_nextQueueIndex.fetch_add(1, std::memory_order_relaxed) % m_taskQueues.size(); } m_taskQueues[queueIndex]->push([task]() { (*task)(); }); return result; }
3. 可选优化:修复Trie树清理逻辑
Scanner::clearTrie函数在删除子节点后未清空父节点的childs映射,虽不影响崩溃修复,但可避免悬空指针:
void Scanner::clearTrie(TrieNode* root) { if (root == nullptr) return; for (auto iter = root->childs.begin(); iter != root->childs.end(); ++iter) { clearTrie(iter.key()); } root->childs.clear(); delete root; }
完整代码实现
workstealthreadpool.h
#pragma once #include <deque> #include <functional> #include <mutex> #include <thread> #include <future> #include <vector> #include <atomic> class WorkStealQueue { public: WorkStealQueue() = default; WorkStealQueue(const WorkStealQueue& rhs) = delete; WorkStealQueue& operator=(const WorkStealQueue& rhs) = delete; ~WorkStealQueue() = default; void push(std::function<void()> task); bool pop(std::function<void()>& task); bool steal(std::function<void()>& task); private: std::deque<std::function<void()>> m_tasks; std::mutex m_mutex; }; class WorkStealThreadPool { public: explicit WorkStealThreadPool(std::size_t threadNums) : m_stop(false), m_nextQueueIndex(0) { init(threadNums); } ~WorkStealThreadPool(); template<typename Callback, typename... Args> auto addTask(Callback&& func, Args&&... args)->std::future<typename std::result_of<Callback(Args...)>::type>; private: void init(std::size_t threadNums); bool stealTask(std::function<void()>& task); void worker(size_t index); private: std::vector<std::thread> m_workThreads; std::vector<std::unique_ptr<WorkStealQueue>> m_taskQueues; std::atomic<bool> m_stop; std::atomic<std::size_t> m_nextQueueIndex; static thread_local std::size_t m_index; }; template<typename Callback, typename... Args> auto WorkStealThreadPool::addTask(Callback&& func, Args&&... args) -> std::future<typename std::result_of<Callback(Args...)>::type> { using returnType = typename std::result_of<Callback(Args...)>::type; auto task = std::make_shared<std::packaged_task<returnType()>>(std::bind(std::forward<Callback>(func), std::forward<Args>(args)...)); std::future<returnType> result = task->get_future(); std::size_t queueIndex; if (m_index < m_taskQueues.size()) { queueIndex = m_index; } else { queueIndex = m_nextQueueIndex.fetch_add(1, std::memory_order_relaxed) % m_taskQueues.size(); } m_taskQueues[queueIndex]->push([task]() { (*task)(); }); return result; }
workstealthreadpool.cpp
#include "workstealthreadpool.h" #include <limits> void WorkStealQueue::push(std::function<void()> task) { std::lock_guard<std::mutex> lock(m_mutex); m_tasks.emplace_back(std::move(task)); } bool WorkStealQueue::pop(std::function<void()>& task) { std::lock_guard<std::mutex> lock(m_mutex); if (m_tasks.empty()) return false; task = std::move(m_tasks.front()); m_tasks.pop_front(); return true; } bool WorkStealQueue::steal(std::function<void()>& task) { std::lock_guard<std::mutex> lock(m_mutex); if (m_tasks.empty()) return false; task = std::move(m_tasks.back()); m_tasks.pop_back(); return true; } thread_local std::size_t WorkStealThreadPool::m_index = std::numeric_limits<std::size_t>::max(); void WorkStealThreadPool::init(std::size_t threadNums) { for (std::size_t i = 0; i < threadNums; ++i) { m_taskQueues.emplace_back(std::make_unique<WorkStealQueue>()); m_workThreads.emplace_back(&WorkStealThreadPool::worker, this, i); } } WorkStealThreadPool::~WorkStealThreadPool() { m_stop = true; for (std::thread& workerThread : m_workThreads) { if (workerThread.joinable()) { workerThread.join(); } } } bool WorkStealThreadPool::stealTask(std::function<void()>& task) { for (std::size_t i = 0; i < m_taskQueues.size(); ++i) { std::size_t index = (m_index + i + 1) % m_taskQueues.size(); if (m_taskQueues[index]->steal(task)) { return true; } } return false; } void WorkStealThreadPool::worker(std::size_t index) { m_index = index; while (!m_stop) { std::function<void()> task; if (m_taskQueues[m_index]->pop(task) || stealTask(task)) { task(); } else { std::this_thread::yield(); } } }
scanner.h
#pragma once #include "workstealthreadpool.h" #include <QMap> #include <QString> struct TrieNode { QMap<TrieNode*, QString> childs; TrieNode* parent = nullptr; }; class Scanner { public: explicit Scanner(std::size_t threadNums); virtual ~Scanner(); virtual void scanDrives(const QStringList& drives); virtual bool isScanCompleted(); virtual std::vector<TrieNode*> fetchScanResults(); private: void scanCore(const QString& currentPath, TrieNode* parent); void clearTrie(TrieNode* root); private: TrieNode* m_root; WorkStealThreadPool* m_threadPool; std::mutex m_mutex; std::vector<TrieNode*> m_fileNodes; std::atomic<int> m_taskCount; }; #ifdef _DEBUG void Print(TrieNode* root); void Print(std::vector<TrieNode*>* fileNodes); #endif
scanner.cpp
#include "scanner.h" #include <QDir> Scanner::Scanner(std::size_t threadNums) : m_root(nullptr) , m_threadPool(nullptr) , m_taskCount(0) { if (threadNums != 0) { m_threadPool = new WorkStealThreadPool(threadNums); } } Scanner::~Scanner() { delete m_threadPool; clearTrie(m_root); } void Scanner::scanDrives(const QStringList& drives) { clearTrie(m_root); m_fileNodes.clear(); m_root = new TrieNode(); for (const QString& drive : drives) { TrieNode* child = new TrieNode(); child->parent = m_root; m_root->childs[child] = drive; scanCore(drive, child); } } bool Scanner::isScanCompleted() { return m_taskCount.load(std::memory_order_acquire) == 0; } std::vector<TrieNode*> Scanner::fetchScanResults() { if (!isScanCompleted()) { std::lock_guard<std::mutex> lock(m_mutex); return m_fileNodes; } return m_fileNodes; } void Scanner::scanCore(const QString& currentPath, TrieNode* parent) { QDir dir(currentPath); if (!dir.exists()) return; QStringList fileNames = dir.entryList(QDir::Files); for (const QString& fileName : fileNames) { TrieNode* child = new TrieNode(); child->parent = parent; parent->childs[child] = fileName; std::lock_guard<std::mutex> lock(m_mutex); m_fileNodes.emplace_back(child); } QStringList subdirNames = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& subdirName : subdirNames) { QString childPath = currentPath + QDir::separator() + subdirName; TrieNode* child = new TrieNode(); child->parent = parent; parent->childs[child] = subdirName + "/"; if (m_threadPool) { m_taskCount.fetch_add(1, std::memory_order_release); m_threadPool->addTask([this, childPath, child] { scanCore(childPath, child); m_taskCount.fetch_sub(1, std::memory_order_acquire); } ); } else { scanCore(childPath, child); } } } void Scanner::clearTrie(TrieNode* root) { if (root == nullptr) return; for (auto iter = root->childs.begin(); iter != root->childs.end(); ++iter) { clearTrie(iter.key()); } root->childs.clear(); delete root; } #ifdef _DEBUG void Print(TrieNode* root) { static int level = 0; if (root == nullptr || root->childs.empty()) return; for (auto iter = root->childs.begin(); iter != root->childs.end(); ++iter) { qDebug().noquote() << QString(" ").repeated(level) << iter.value(); ++level; Print(iter.key()); --level; } } #endif #ifdef _DEBUG void Print(std::vector<TrieNode*>* fileNodes) { for (TrieNode* fileNode : *fileNodes) { qDebug().noquote() << fileNode->parent->childs[fileNode]; } } #endif
main.cpp
#include "scanner.h" #include <QStorageInfo> #include <chrono> void TestScanDrives(Scanner& scanner, const QStringList& drives) { scanner.scanDrives(drives); while (!scanner.isScanCompleted()) { std::this_thread::sleep_for(std::chrono::seconds(1)); } } int main() { QStringList drives; for (const QStorageInfo& drive : QStorageInfo::mountedVolumes()) { if (drive.isValid() && drive.isReady()) { drives << drive.rootPath(); } } for (int i = 2; i <= 10; i += 2) { Scanner scanner(i); TestScanDrives(scanner, drives); } return 0; }
内容的提问来源于stack exchange,提问作者lijiang99

