
1. 项目概述为什么C11的thread是游戏规则的改变者在C11标准之前C的多线程编程可以说是一场“黑暗森林”里的冒险。你得依赖操作系统提供的原生API比如Windows的CreateThread或者Linux的pthread_create。这不仅让代码充满了平台相关的宏定义和条件编译也让线程的创建、同步和管理变得异常繁琐移植性更是一塌糊涂。我记得那时候写跨平台的多线程代码光是处理不同系统的线程句柄和属性就能写出一大堆让人头疼的胶水代码。C11引入的std::thread就像是给这片黑暗森林带来了一束标准化的光。它把线程抽象成了一个标准的类让创建和管理线程变得和操作其他STL容器一样直观。这不仅仅是语法糖更是一种编程范式的转变——将多线程支持从“库特性”提升到了“语言核心特性”的层面。这意味着编译器厂商、标准库实现者都必须提供稳定、高效的支持我们开发者终于可以写一份代码在主流平台上都能获得一致的多线程行为。今天我们就来彻底拆解std::thread创建线程的三种核心方式这不仅是入门的基础更是理解现代C并发编程思想的钥匙。无论你是刚接触并发的新手还是想从老式API迁移过来的老鸟掌握这三种方式都能让你在编写高性能、可维护的并发代码时更加得心应手。2. 核心思路三种创建方式的本质与选型逻辑std::thread构造函数的设计非常巧妙它利用了C11的可变参数模板和完美转发机制使得我们可以用几乎任意可调用对象来启动一个新线程。这三种创建方式表面上是语法不同但背后对应的是三种不同的代码组织思想和资源管理策略。理解为什么会有这三种方式比死记硬背语法更重要。第一种函数指针这是最传统、最直接的方式。它代表了C语言以来的过程式编程思想。函数是独立的、无状态的执行单元。这种方式适合那些逻辑独立、不依赖于外部状态的纯函数任务。它的优点是简单明了职责清晰缺点也很明显如果函数需要访问或修改某些共享数据就必须通过参数传入对于复杂状态的管理不够灵活。第二种函数对象仿函数这是C面向对象和泛型编程思想的体现。一个重载了operator()的类本质上是一个有状态的函数。它允许你将数据和操作封装在一起在线程启动前就初始化好所需的状态。这种方式非常适合需要配置参数、或者需要维护内部状态的线程任务。相比于函数指针它提供了更好的封装性和灵活性。第三种Lambda表达式这是C11现代编程风格的集大成者。它本质上是第二种方式函数对象的语法糖但更加简洁和强大。你可以在需要线程逻辑的地方就地定义并且能通过捕获列表直接“捕获”当前作用域的变量。这极大地简化了代码尤其是当你需要给线程传递一个轻量级的、一次性使用的任务时。Lambda是现代C并发编程中最常用、最推荐的方式。选择哪种方式没有绝对的好坏关键在于场景追求极简和清晰使用函数指针。需要封装复杂状态和行为使用函数对象。快速实现尤其是需要捕获局部变量毫不犹豫地使用Lambda。3. 方式一使用函数指针——最经典的传承这是从C语言时代延续下来的方式也是理解线程执行模型最直观的起点。你只需要一个普通的全局函数或静态成员函数把它的地址传给std::thread的构造函数即可。3.1 基础语法与示例假设我们有一个简单的任务打印线程ID和一些信息。#include iostream #include thread #include chrono // 一个普通的全局函数作为线程入口点 void printMessage(const std::string msg, int count) { for (int i 0; i count; i) { std::cout Thread ID: std::this_thread::get_id() | Message: msg ( i1 / count )\n; std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟耗时 } } int main() { std::cout Main thread ID: std::this_thread::get_id() std::endl; // 创建线程传入函数指针和参数 std::thread t1(printMessage, Hello from Thread!, 5); // 主线程继续执行其他工作 for (int i 0; i 3; i) { std::cout Main thread is working...\n; std::this_thread::sleep_for(std::chrono::milliseconds(200)); } // 等待子线程t1执行完毕 t1.join(); std::cout All threads finished.\n; return 0; }在这段代码中std::thread t1(printMessage, “Hello from Thread!”, 5);这一行就是精髓。构造函数第一个参数是函数指针printMessage后面的参数”Hello from Thread!”和5会通过完美转发传递给printMessage函数。新线程t1会立即开始执行printMessage函数而主线程继续向下执行。3.2 参数传递的深层机制与避坑指南这里有一个至关重要的细节线程函数的参数是以值传递by value的方式被复制或移动到新线程的内部存储中的。这意味着即使你传递了一个引用如const std::string实际上线程内部得到的是这个引用所绑定对象的副本。理解这一点是避免踩坑的关键。坑点1试图传递非常量引用以修改主线程变量void increment(int x) { x; } int main() { int value 10; // 错误编译可能通过但行为错误。线程内修改的是value的副本主线程的value不变。 // std::thread t(increment, value); // t.join(); // std::cout value std::endl; // 输出仍然是10 // 正确做法使用 std::ref 包装引用 std::thread t(increment, std::ref(value)); t.join(); std::cout value std::endl; // 输出是11 return 0; }注意如果你确实需要在线程间通过引用共享并修改数据必须使用std::ref或std::cref用于常量引用来包装参数。这告诉std::thread“不要复制这个参数请传递它的引用”。但请注意这引入了共享数据你必须小心数据竞争通常需要配合互斥锁std::mutex来使用。坑点2传递指针与生命周期管理传递指针是允许的但你必须绝对确保指针所指向的对象在线程执行的整个生命周期内都是有效的。否则就是悬垂指针会导致未定义行为通常是程序崩溃。void processData(int* data) { std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout “Data: ” *data std::endl; // 危险data可能已失效 } int main() { int* p new int(42); std::thread t(processData, p); delete p; // 错误在线程结束前释放了内存。 t.join(); return 0; }实操心得对于需要通过指针传递的堆上对象考虑使用std::shared_ptr或std::unique_ptr配合移动语义来管理生命周期让智能指针的拷贝和析构来保证资源安全。或者更简单的方法是尽量避免在线程间传递原始指针优先使用值传递或引用传递配合std::ref。坑点3重载函数歧义如果你的函数有重载版本编译器可能无法推断出你要使用哪一个。这时需要使用静态转型来明确指定函数类型。void task(int) { /* ... */ } void task(double) { /* ... */ } int main() { // 错误编译器不知道选哪个task // std::thread t(task, 10); // 正确使用static_cast指定函数指针类型 std::thread t(static_castvoid(*)(int)(task), 10); t.join(); return 0; }4. 方式二使用函数对象仿函数——封装的艺术函数对象是一个类或结构体它重载了函数调用运算符operator()。这使得该类的实例可以像函数一样被调用。这种方式将“数据”成员变量和“操作”operator()封装在一起非常适合需要携带状态的线程任务。4.1 基本实现与线程创建#include iostream #include thread #include vector // 1. 定义一个函数对象类 class Counter { private: int m_id; // 状态计数器ID int m_count; // 状态计数值 public: // 构造函数初始化状态 Counter(int id, int count) : m_id(id), m_count(count) { std::cout “Counter ” m_id “ constructed.\n”; } // 重载函数调用运算符定义线程要执行的任务 void operator()() const { for (int i 0; i m_count; i) { std::cout “Counter ” m_id “: ” i std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(50)); } std::cout “Counter ” m_id “ finished.\n”; } // 可以重载带参数的 operator() void operator()(const std::string prefix) const { for (int i 0; i m_count; i) { std::cout prefix “ Counter ” m_id “: ” i std::endl; } } }; int main() { // 2. 创建函数对象实例 Counter counter1(1, 5); Counter counter2(2, 3); // 3. 将函数对象实例传递给 std::thread // 注意这里传递的是 counter1 的拷贝 std::thread t1(counter1); // 传递带额外参数的版本 std::thread t2(std::ref(counter2), “[Thread-2] “); // 使用ref避免拷贝直接操作原对象 // 主线程等待 t1.join(); t2.join(); std::cout “Main thread: All counters done.\n”; // 4. 临时对象右值的用法 std::thread t3(Counter(3, 4)); // 创建一个临时Counter对象并移动或拷贝到线程中 t3.join(); return 0; }关键点在于std::thread t1(counter1);。当counter1作为参数传递时它会被拷贝到新线程的内部存储中。随后新线程会调用这个副本的operator()。这就是为什么Counter的拷贝构造函数如果用户没定义编译器会生成一个必须是可用的并且operator()最好是const的因为它操作的是副本的数据。4.2 拷贝与移动语义的深入探讨这是使用函数对象时最需要厘清的概念。std::thread的构造函数对于传入的可调用对象会尝试进行拷贝或移动。拷贝构造如果你传递了一个左值如上面例子中的counter1那么会调用该函数对象的拷贝构造函数来创建副本。这意味着你的类必须是可拷贝的CopyConstructible。移动构造如果你传递了一个右值如临时对象Counter(3, 4)或者使用std::move那么会调用移动构造函数。这通常效率更高因为它可以“窃取”临时对象的资源避免不必要的深拷贝。一个常见的错误是函数对象包含了不可拷贝的资源比如文件句柄、网络连接或std::unique_ptr。class TaskWithUniqueResource { std::unique_ptrint m_resource; public: TaskWithUniqueResource(int val) : m_resource(std::make_uniqueint(val)) {} void operator()() { /* 使用 m_resource */ } }; int main() { TaskWithUniqueResource task(42); // std::thread t(task); // 编译错误std::unique_ptr 不可拷贝因此 TaskWithUniqueResource 也不可拷贝。 // 正确使用 std::move 将其转换为右值触发移动构造 std::thread t(std::move(task)); // 注意此时 task 对象内部的 m_resource 已经为空不能再被主线程使用。 t.join(); return 0; }注意事项当你使用std::move将对象移入线程后原始对象就处于“被移动”状态通常为空或无效。在主线程中继续使用这个原始对象是危险的会导致未定义行为。这是一种所有权的转移资源的所有权从主线程的task对象转移到了新线程内部的副本中。最佳实践建议对于复杂的函数对象如果其状态很大或包含不可拷贝的资源应优先设计其移动构造函数并在创建线程时使用std::move来传递以获得最佳性能。同时在类定义中可以考虑将operator()声明为const以明确表示它不会修改对象状态尽管操作的是副本这有助于编译器优化和代码理解。5. 方式三使用Lambda表达式——现代C的利器Lambda表达式是C11最伟大的特性之一它让定义匿名函数对象变得极其方便。对于线程编程来说它允许你将线程逻辑内联地写在std::thread的构造函数旁边并且能自然地捕获上下文变量代码紧凑且意图清晰。5.1 Lambda捕获列表与线程安全初探Lambda的核心语法是[捕获列表](参数列表) - 返回类型 { 函数体 }。其中“捕获列表”决定了Lambda函数体如何访问外部作用域的变量。#include iostream #include thread #include vector int main() { int sharedCounter 0; const int numThreads 10; std::vectorstd::thread threads; // 错误示范通过引用捕获共享变量但没有同步导致数据竞争 // for (int i 0; i numThreads; i) { // threads.emplace_back([sharedCounter, i]() { // [] 捕获所有外部变量引用 // for (int j 0; j 1000; j) { // sharedCounter; // 多个线程同时修改结果不确定 // } // std::cout “Thread ” i “ finished.\n”; // }); // } // 正确做法1通过值捕获每个线程有自己的副本适用于独立计数 int localCounter 0; for (int i 0; i numThreads; i) { threads.emplace_back([localCounter, i]() mutable { // [] 捕获所有外部变量值 int myCounter localCounter; // 每个线程从localCounter的初始值0开始 for (int j 0; j 1000; j) { myCounter; } std::cout “Thread ” i “ myCounter: ” myCounter std::endl; // localCounter 是副本修改它不影响主线程的 localCounter }); } for (auto t : threads) { t.join(); } std::cout “Main thread localCounter: ” localCounter std::endl; // 输出 0 threads.clear(); // 正确做法2通过引用捕获但使用互斥锁进行同步 std::mutex counterMutex; sharedCounter 0; for (int i 0; i numThreads; i) { threads.emplace_back([sharedCounter, counterMutex, i]() { // [] 捕获引用 for (int j 0; j 1000; j) { std::lock_guardstd::mutex lock(counterMutex); // 加锁 sharedCounter; // 受保护的修改 } std::cout “Thread ” i “ contributed.\n”; }); } for (auto t : threads) { t.join(); } std::cout “Final sharedCounter (with mutex): ” sharedCounter std::endl; // 输出 10000 return 0; }捕获方式详解[]以值拷贝的方式捕获所有外部变量。在Lambda体内修改这些副本需要mutable关键字且修改不影响外部变量。[]以引用的方式捕获所有外部变量。修改它们会影响外部变量但必须极其小心数据竞争。[var]或[var]显式地指定以值或引用方式捕获特定变量。这是推荐的做法明确意图避免意外捕获。[this]捕获当前类对象的指针使得Lambda可以访问类的成员变量和函数。[, var]默认以值捕获但var以引用捕获。混合捕获提供了灵活性。重要心得在并发编程中默认使用值捕获[]或显式列出变量通常是更安全的选择因为它避免了意外的共享数据修改。只有当你有意让多个线程共享并修改同一数据并且已经设计了完善的同步机制如互斥锁、原子操作时才使用引用捕获。对于简单的计数器C11提供了std::atomic这是比“互斥锁普通变量”更高效的选择。5.2 Lambda的泛型与完美转发C14引入了泛型Lambda参数可以使用autoC20更是增强了其能力。结合std::thread的完美转发我们可以写出非常灵活和高效的代码。#include iostream #include thread #include string // 一个模拟的、可以处理任意可调用对象的任务执行器 void executeTask(std::thread t) { // ... 可能将线程加入线程池管理 t.join(); } int main() { std::string config “HighPriority”; // 使用泛型Lambda和完美转发参数 auto task [config](auto func, auto... args) { std::cout “Task started with config: ” config std::endl; // 完美转发参数给传入的函数 std::invoke(std::forwarddecltype(func)(func), std::forwarddecltype(args)(args)...); std::cout “Task finished.\n”; }; // 定义一个简单的函数 auto sayHello [](const std::string name, int times) { for (int i 0; i times; i) { std::cout “Hello, ” name “!\n”; } }; // 创建线程Lambda捕获了task和config并接受sayHello及其参数 // 注意这里task本身是一个Lambda它又接受一个可调用对象和其参数。 // std::thread 会拷贝或移动这个捕获了外部变量的Lambda对象。 std::thread t([task std::move(task), sayHello]() { // C14 初始化捕获移动task task(sayHello, “Alice”, 3); // 在子线程中调用tasktask再调用sayHello }); t.join(); // 更简洁的写法直接在thread构造函数中写Lambda并传递参数 std::string userName “Bob”; std::thread t2([](const std::string name) { std::cout “Greetings from thread, ” name “!\n”; }, std::ref(userName)); // 注意这里用std::ref传递userName的引用 t2.join(); return 0; }这个例子展示了Lambda的强大之处你可以创建一个高阶的Lambdatask它接受另一个函数作为参数。通过初始化捕获[task std::move(task)]我们甚至可以将Lambda自身移动进新线程的上下文中实现灵活的任务组合和调度。std::invoke和完美转发std::forward的运用使得这个任务执行器可以处理几乎任何类型的可调用对象和参数。6. 线程管理、资源与生命周期实战创建线程只是第一步如何管理它的生命周期确保资源正确释放是更关键的一环。std::thread对象本身是资源句柄它关联着一个底层操作系统线程。6.1 join() 与 detach() 的抉择与陷阱每个std::thread对象都处于两种状态之一可连接joinable或不可连接unjoinable。新创建的、未被移动过的、且底层线程仍在运行的thread对象是可连接的。join()阻塞当前线程通常是主线程直到被join的线程执行完毕。这确保了子线程的资源如栈内存被正确清理。join()之后thread对象变为不可连接状态。detach()将thread对象与其底层执行线程分离。分离后底层线程会独立运行其资源在线程结束时由系统自动回收。detach()之后thread对象也变为不可连接状态。你无法再对它进行join也无法获取其状态。一个铁律在std::thread对象销毁之前你必须决定它的命运——要么join()要么detach()。如果对一个可连接的thread对象既不join也不detach那么在其析构函数中std::terminate()会被调用导致整个程序异常终止。这是C标准强制规定的安全措施防止你无意中造成线程泄露类似于内存泄露但更隐蔽和危险。#include iostream #include thread #include chrono void riskyDetach() { std::thread t([](){ std::this_thread::sleep_for(std::chrono::seconds(2)); std::cout “Detached thread finished.\n”; // 可能永远看不到这行输出 }); t.detach(); // 主线程立即返回不等待t。 // t 对象在此作用域结束被销毁但底层线程可能还在运行。 std::cout “riskyDetach function exits immediately.\n”; // 如果主程序在2秒前结束那个detached线程可能被强行终止其输出可能丢失。 } void safeScopedThread() { int localVar 100; // 使用RAII思想包装thread确保在作用域结束时join class ThreadGuard { std::thread m_t; public: explicit ThreadGuard(std::thread t) : m_t(t) {} ~ThreadGuard() { if (m_t.joinable()) { m_t.join(); // 确保线程结束 } } ThreadGuard(const ThreadGuard) delete; ThreadGuard operator(const ThreadGuard) delete; }; std::thread t([localVar](){ std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout “Scoped thread sees localVar: ” localVar std::endl; }); ThreadGuard g(t); // 守卫对象析构时自动join // 即使这里抛出异常g的析构函数也会被调用从而join线程。 std::cout “safeScopedThread doing other work...\n”; // 函数结束g析构t被join。 } int main() { std::cout “ Example of risky detach \n”; riskyDetach(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 给detached线程一点输出机会 std::cout “\n Example of RAII guard \n”; safeScopedThread(); std::cout “\nMain thread waiting a bit to see if detached thread outputs...\n”; std::this_thread::sleep_for(std::chrono::seconds(3)); // 等待足够长时间 return 0; }核心建议默认总是使用join()。detach()只适用于那些“发射后不管”的后台任务并且你必须非常清楚该任务不会访问即将失效的局部变量尤其是引用捕获的Lambda。在实际项目中使用detach()的情况远少于join()。更好的做法是使用std::async它返回std::future或者像上面ThreadGuard那样的RAII包装类来自动管理线程生命周期。6.2 线程标识、硬件并发数与实践建议每个运行的线程都有一个唯一的标识符可以通过std::this_thread::get_id()获取也可以从std::thread对象通过t.get_id()获取。这个ID在调试和日志中非常有用。std::thread::hardware_concurrency()是一个静态函数它返回当前系统支持的真正并发运行的线程数通常是CPU核心数或超线程数。这个值是创建线程池或进行任务划分时的重要参考。#include iostream #include thread #include vector void worker(int id) { std::cout “Worker ” id “ started on thread ” std::this_thread::get_id() std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(100 * id)); std::cout “Worker ” id “ finished.\n”; } int main() { // 获取硬件支持的并发数 unsigned int numCores std::thread::hardware_concurrency(); std::cout “This system can run ” numCores “ threads concurrently.\n”; // 创建略多于核心数的线程观察操作系统调度 std::vectorstd::thread threads; int numWorkers numCores 2; std::cout “Creating ” numWorkers “ worker threads...\n”; for (int i 0; i numWorkers; i) { threads.emplace_back(worker, i); } std::cout “Main thread ID: ” std::this_thread::get_id() std::endl; for (auto t : threads) { t.join(); } std::cout “All workers joined.\n”; // 演示线程ID的比较 std::thread t1([]{}); std::thread t2([]{}); std::cout “t1 ID t2 ID ? ” (t1.get_id() t2.get_id()) std::endl; // 输出 0 (false) std::cout “Main thread ID std::this_thread::get_id() ? ” (std::this_thread::get_id() std::this_thread::get_id()) std::endl; // 输出 1 (true) t1.join(); t2.join(); return 0; }实践中的黄金法则线程数量不是越多越好。创建远超硬件并发数的线程会导致大量的上下文切换开销反而降低性能。通常线程池的大小设置为hardware_concurrency()或略多一点是一个好的起点。使用RAII管理线程。避免裸的std::thread变量在函数间传递或存储在复杂数据结构中。考虑使用std::jthreadC20引入自带取消功能和RAII join或者自己封装一个守卫类。警惕线程与异常。如果创建线程后在join()之前代码抛出了异常并且异常未被捕获那么join()可能永远不会被调用导致程序终止。因此确保在可能抛出异常的代码路径上使用RAII或try-catch块来保证join。std::thread t([]{ /* ... */ }); try { // ... 可能抛出异常的代码 t.join(); } catch (...) { t.join(); // 在catch块中也join throw; // 重新抛出异常 }7. 综合对比、性能考量与高级模式现在让我们将三种方式放在一起从多个维度进行对比并探讨一些更高级的应用模式。7.1 三种方式对比速查表特性维度函数指针函数对象仿函数Lambda表达式语法简洁性中等。需提前定义函数。较繁琐。需定义完整的类。最优。就地定义极其简洁。状态封装能力无。状态必须通过参数传递。强大。成员变量可封装任意复杂状态。强大。通过捕获列表封装状态灵活。代码组织分散。逻辑在独立的函数中。集中。数据和行为封装在类中。内联。逻辑紧挨着使用它的地方。性能开销低。仅函数调用开销。可能有一次对象拷贝/移动开销。同函数对象通常编译器优化很好。适用场景简单的、无状态的工具函数。复杂的、需配置的、可重用的任务单元。绝大多数场景特别是短小的一次性任务。可读性与维护性函数名需清晰否则跳转查看。类名即意图但代码量多。高。逻辑和上下文在一起一目了然。闭包访问局部变量不支持。间接支持通过构造函数传入。原生支持。通过捕获列表直接访问。从上表可以清晰看出Lambda表达式在现代C多线程编程中占据了绝对主导地位。它完美平衡了表达能力、简洁性和性能。函数指针因其局限性通常只用于兼容旧代码或极简单的回调。函数对象则在需要定义复杂、可配置、可复用的“任务类”时仍有其价值。7.2 从thread到async更高层次的抽象对于许多“启动一个任务并获取结果”的场景std::async是比直接使用std::thread更高级、更安全的选择。它返回一个std::future可以方便地获取异步操作的结果并且内部可能使用线程池来管理资源效率更高。#include iostream #include future #include chrono int computeHeavyTask(int input) { std::this_thread::sleep_for(std::chrono::seconds(1)); return input * input; } int main() { // 使用 std::async 异步执行任务 // std::launch::async 策略保证在新线程中执行 std::futureint fut std::async(std::launch::async, computeHeavyTask, 12); // 主线程可以继续做其他工作 std::cout “Main thread is working...\n”; std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 当需要结果时调用 get()如果还没算完会阻塞等待 int result fut.get(); std::cout “The result is: ” result std::endl; // 输出 144 // 也可以和Lambda一起用 auto fut2 std::async(std::launch::async, [](int a, int b) { return a b; }, 20, 22); std::cout “Another result: ” fut2.get() std::endl; // 输出 42 return 0; }std::async自动管理了线程的创建和清理通过future的析构函数隐含的wait避免了手动join的麻烦和风险。在不需要精细控制线程只是想要异步执行并获取结果的场景下应优先考虑std::async。7.3 线程池模式下的任务封装在实际的大型项目中直接创建大量std::thread是低效的。线程池模式预先创建一组线程然后通过任务队列向其提交任务通常就是可调用对象。这时三种创建方式定义的任务都可以作为任务提交。// 一个极简的任务队列和线程池示意 #include iostream #include thread #include vector #include queue #include functional #include mutex #include condition_variable #include atomic class SimpleThreadPool { public: using Task std::functionvoid(); // 任务类型无参数无返回的可调用对象 SimpleThreadPool(size_t numThreads) : m_done(false) { for (size_t i 0; i numThreads; i) { m_threads.emplace_back(SimpleThreadPool::workerThread, this); } } ~SimpleThreadPool() { { std::lock_guardstd::mutex lock(m_queueMutex); m_done true; } m_condition.notify_all(); // 唤醒所有等待的线程 for (auto t : m_threads) { if (t.joinable()) t.join(); } } void submit(Task task) { { std::lock_guardstd::mutex lock(m_queueMutex); m_taskQueue.push(std::move(task)); } m_condition.notify_one(); // 唤醒一个等待的线程 } private: void workerThread() { while (true) { Task task; { std::unique_lockstd::mutex lock(m_queueMutex); // 等待条件任务队列非空或线程池已关闭 m_condition.wait(lock, [this]() { return !m_taskQueue.empty() || m_done; }); if (m_done m_taskQueue.empty()) { return; // 线程池关闭且无任务线程退出 } task std::move(m_taskQueue.front()); m_taskQueue.pop(); } // 执行任务三种方式创建的任务在这里被统一调用 task(); } } std::vectorstd::thread m_threads; std::queueTask m_taskQueue; std::mutex m_queueMutex; std::condition_variable m_condition; std::atomicbool m_done; }; int main() { SimpleThreadPool pool(4); // 创建4个线程的池子 // 提交使用函数指针定义的任务 pool.submit([](){ std::cout “Task A (Lambda)\n”; }); // 提交使用函数对象定义的任务 struct MyFunctor { void operator()() const { std::cout “Task B (Functor)\n”; } }; pool.submit(MyFunctor{}); // 提交使用函数指针定义的任务需要包装一下因为函数指针不是std::functionvoid() void (*funcPtr)() [](){ std::cout “Task C (Function Pointer)\n”; }; pool.submit(funcPtr); // 函数指针可以隐式转换为std::function // 等待一会儿让任务执行 std::this_thread::sleep_for(std::chrono::seconds(1)); return 0; // pool析构时会等待所有线程结束 }在这个线程池模型中std::functionvoid()成为了任务的统一抽象。无论是Lambda、函数对象还是函数指针最终都可以包装成这种形式提交到任务队列中。这充分体现了C多态和泛型的强大创建方式可以多种多样但管理和执行的模式可以统一和标准化。