线程池项目2
一、模块划分与职责模块作用Any类型擦除容器用于存储任务返回的任意类型值支持安全类型转换。Semaphore基于mutexcondition_variable实现的计数信号量用于线程间同步。Task任务抽象基类用户继承并实现run()通过exec()触发执行并设置结果。Result任务返回值载体内部含信号量用户调用get()阻塞等待结果到达。Thread轻量线程封装存储线程函数和自增ID启动时detach分离。ThreadPool核心管理类任务队列、线程池、模式切换、动态扩缩容、生命周期控制。二、核心机制详解1. 生产者-消费者模型生产者用户调用submitTask将任务shared_ptrTask放入任务队列。消费者池内所有线程运行threadFunc循环从队列中取任务执行。线程安全任务队列由taskQueMtx_互斥锁保护配合notEmpty_和notFull_条件变量实现阻塞/通知。2. 两种工作模式MODE_FIXED固定模式线程数量在start()时确定永不增减。任务队列满时提交者阻塞最长1秒。MODE_CACHED缓存模式动态扩容当taskSize_ idleThreadSize_且当前线程数未达上限时立即创建新线程。空闲回收线程在无任务时使用wait_for超时等待若空闲超过THREAD_MAX_IDLE_TIME60秒且当前线程数 初始线程数则线程自我销毁从threads_中删除并退出。3. 异步结果获取每个任务提交时返回一个Result对象内部持有指向该任务的shared_ptr和一个信号量。任务执行完时Task::exec()调用run()得到Any返回值再通过Result::setVal存储并post信号量。用户线程调用Result::get()会wait信号量直到结果就绪。4. 线程生命周期管理启动start(initThreadSize)创建Thread对象每个对象启动时detach分离线程线程函数绑定到ThreadPool::threadFunc。正常退出析构函数设置isPoolRunning_ false唤醒所有等待线程每个线程检测到标志后从threads_中删除自身最后通知exitCond_主线程等待所有线程退出。缓存模式主动回收空闲超时的线程直接return同样会执行threads_.erase(threadid)并修改计数。5. 任务队列的背压控制提交任务时最多等待1秒直到队列有空间notFull_.wait_for否则返回无效ResultisValid_false。取出任务后若队列仍非空则通知其他线程继续取任务同时通知notFull_让可能阻塞的生产者继续提交。三、设计亮点与权衡类型擦除的返回值Any类通过多态 模板派生避免了void*的不安全性用户获取时需显式指定类型cast_T()若类型不匹配则抛异常。信号量自实现C11 标准库无信号量用mutexcondition_variable封装了一个简洁的Semaphore用于Result中实现单次同步。线程ID的自定义映射每个Thread有自增整型ID线程池用unordered_mapint, unique_ptrThread存储线程函数通过传入的threadid参数在退出时删除自身条目。注意这里依赖“线程函数参数就是 map 的 key”这一约定且删除操作在持有锁时进行避免了并发问题。原子操作与锁的精细使用taskSize_、curThreadSize_、idleThreadSize_使用atomic_int减少锁竞争但任务队列的操作仍必须加锁以保证条件变量的正确性。资源回收的同步析构时使用exitCond_等待所有线程退出避免detach后主线程退出导致进程 crash。每个线程退出前都会notify_all确保主线程被唤醒。可扩展性用户只需继承Task并实现run()即可定义任意业务逻辑线程池只负责调度和结果传递。问题一:如果把线程池的任务队列从 std::shared_ptrTask 改成裸指针 std::queueTask*会怎么样void someFunction() { MyTask task; // 栈上的临时对象 pool.submitTask(task); // 传入裸指针 } // 函数结束task 被销毁线程池内部将task存入std::queueTask*。someFunction执行完毕后task对象生命周期结束内存被回收或栈空间被复用。队列中只剩下一个指向已销毁对象的指针。void someFunction() { auto sp std::make_sharedMyTask(); // 堆对象引用计数 1 pool.submitTask(sp); // 传入 sp左值拷贝 } // someFunction 结束sp 销毁引用计数减 1但队列中还有一份对象不销毁在拷贝的时候计数器会加1,所以不用担心对象会销毁问题二:为什么要自己封装一个Thread类呢封装Thread类的根本原因是给系统线程附加一个整数 ID以便在线程池中高效地标识、管理和回收特定线程。具体来说std::thread本身不提供直接可用的整数标识符其get_id()返回的std::thread::id是一个不透明的结构体不能直接作为unordered_map的键需要特化hash也无法方便地打印为整数用于日志或调试。通过自定义Thread类作者实现了自增整数 IDstatic int generateId_确保每个线程获得一个唯一的整数 ID可以直接作为unordered_mapint, unique_ptrThread的键。ID 传递到线程函数启动std::thread时将整数 ID 作为参数传递给线程函数threadFunc(int threadid)这样线程函数就能知道自己是谁从而在需要退出时安全地调用threads_.erase(threadid)删除自身。封装detach行为让线程池无需显式管理每个线程的join简化生命周期控制。如果不封装直接用vectorstd::thread线程函数就无法获得一个简单的整数索引来从容器中删除自己管理起来会复杂且低效。因此封装Thread是一种轻量级、实用的设计模式。问题三:把线程函数写进Thread会带来的问题如果Thread内部直接写死任务循环逻辑那么Thread就必须知道任务队列、互斥锁、条件变量、线程池模式等细节。这意味着Thread无法脱离ThreadPool独立使用也失去了复用性。问题四:为什么要绑定Thread类想要调用ThreadPool类的成员函数threadFunc但成员函数必须通过某个ThreadPool对象来调用需要this指针而Thread的接口只接受一个参数为int的普通函数对象。通过std::bind或 lambda将this绑定进去生成一个符合void(int)签名的新可调用对象就能让Thread在启动时传入整数 ID正确调用到对应线程池实例的成员函数。如果不绑定直接传递成员函数指针类型不匹配编译会失败。给包装器类别起个别名问题五:为什么分离线程t是临时变量, 分离线程后即使对象t被销毁, 线程也会独立在后台运行直到它执行的函数func_返回。上边三行的写法相当于下面一行的写法问题:怎么设计run函数的返回值,可以表示任意的类型问题:如何设计Result机制呢用c知识,构建一个Any类型基类指针指向派生类模板构建一个信号量类互斥锁加条件变量