必须在所有使用该资源的线程完全退出后再释放共享对象,否则触发未定义行为;用原子计数器+条件等待控制释放时机,由资源对象自主判断是否可销毁,临界区生命周期须严格长于所有访问线程。

必须在所有使用该资源的线程完全退出后再释放共享对象,否则必然触发未定义行为——比如 DownloadTask 被 delete 后,仍在运行的 HttpDownload 线程继续访问其成员(如 m_cs、m_currentThreads),直接导致崩溃。
用原子计数器 + 条件等待控制释放时机
手动维护线程存活数比依赖 join() 更灵活,尤其在线程由系统管理(如 MFC 的 AfxBeginThread)或需异步终止时。核心是让资源对象自己掌握“是否可销毁”的判断权。
-
std::atomic<int></int>替代裸int m_currentThreads:避免计数器自身成为新的竞态点 - 每个工作线程启动后立即执行
m_task->m_currentThreads.fetch_add(1, std::memory_order_relaxed) - 线程退出前调用
m_task->m_currentThreads.fetch_sub(1, std::memory_order_acq_rel) - 销毁前用
while (m_currentThreads.load(std::memory_order_acquire) != 0) std::this_thread::yield();等待归零
注意:fetch_sub 必须用 memory_order_acq_rel,确保该线程对共享资源的所有写操作(如状态更新、日志记录)在计数减为 0 前对其他线程可见。
避免在析构中隐式等待线程结束
不要在 DownloadTask::~DownloadTask() 里循环等待 m_currentThreads == 0。析构函数被调用时,对象语义已开始失效,此时再访问成员变量(哪怕只是读取 m_currentThreads)存在风险——尤其是当该对象本身也被其他线程通过原始指针引用时。
立即学习“C++免费学习笔记(深入)”;
- 把等待逻辑移到显式销毁入口(如
SetStop()),而非析构函数内 - 确保销毁入口只被调用一次(可用
std::atomic<bool></bool>标记) - 若使用智能指针(如
std::shared_ptr<downloadtask></downloadtask>),需配合weak_ptr防止循环引用导致对象无法释放
临界区与资源释放的生命周期必须严格嵌套
临界区对象(如 CCriticalSection 或 std::mutex)的生命周期必须长于所有可能访问它的线程。一旦锁对象被销毁,后续任何 lock() 或 unlock() 调用都是未定义行为。
- 临界区不能是局部变量,也不能放在即将被
delete的对象内部且晚于该对象析构 - 正确做法:临界区作为
DownloadTask的成员,在DownloadTask析构末尾才销毁;所有HttpDownload线程必须在此前完成并停止访问 - 若用
std::mutex,切勿在持有锁期间调用delete this或销毁锁本身
最容易被忽略的一点:资源释放顺序不是“谁先声明谁先释放”的语法问题,而是“谁最后停止使用,谁决定释放时机”的所有权问题。线程和共享对象之间必须有清晰的生命周期契约,靠计数、标记、回调或消息通知来履行,而不是靠运气或调用顺序假设。


















