目录
方案一:引入C++20协程 (但是好像协程可以与多进程搭配并发)
简易线程池代码
// https://github.com/illjlack/demo
#pragma once
#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>
class ThreadPool
{
public:
ThreadPool(size_t);
~ThreadPool();
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
// 推导返回值类型 (是future包装的F(Args...)的返回值的类型)
// std::future,表示一个异步操作的结果,可以在任务完成后获取返回值。
private:
std::vector<std::thread> workers; // 工作线程
std::queue<std::function<void()>> tasks; // 任务队列
std::mutex queue_mutex; // 任务队列的互斥锁,保护任务队列的访问
std::condition_variable condition; // 条件变量(队列为空或者线程池暂停)
bool stop; // 标志变量,表示线程池是否停止
};
// 构造函数实现:启动指定数量的线程
// 使用内联, 防止链接后重复定义
inline ThreadPool::ThreadPool(size_t threads)
: stop(false)
{
for (size_t i = 0; i < threads; ++i)
{
workers.emplace_back(
[this]
{
for (;;)
{
std::function<void()> task;
{ // 锁的作用域
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(lock,
[this] { return this->stop || !this->tasks.empty(); });
if (this->stop && this->tasks.empty())
{
return;
}
task = std::move(this->tasks.front());
this->tasks.pop();
}
task(); // 执行任务
}
}
);
}
}
// 析构函数实现:停止所有线程并等待它们完成任务
inline ThreadPool::~ThreadPool()
{
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true; // 设置停止标志
}
condition.notify_all(); // 通知所有线程
for (std::thread &worker : workers)
{
worker.join(); // 等待线程完成后关闭线程
}
}
// `enqueue` 方法实现:向线程池的任务队列中添加新任务
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::invoke_result<F, Args...>::type>
{
using return_type = typename std::invoke_result<F, Args...>::type;
// 将任务包装成 `std::packaged_task`,以便获取任务的返回值
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future(); // 获取任务的 future 对象,以便稍后获取任务的结果
{
std::unique_lock<std::mutex> lock(queue_mutex); // 加锁访问任务队列
// 如果线程池已停止,不允许添加新任务
if (stop)
{
throw std::runtime_error("enqueue on stopped ThreadPool");
}
tasks.emplace([task]
{
(*task)();
}); // 将任务加入任务队列
}
condition.notify_one(); // 通知一个等待的工作线程
return res; // 返回任务的 future 对象
}
使用demo
#include <iostream>
#include <vector>
#include <chrono>
#include "ThreadPool.h"
int main()
{
// 创建一个包含4个线程的线程池
ThreadPool pool(4);
// 存储每个任务的结果
std::vector<std::future<int>> results;
// 向线程池添加8个任务
for (int i = 0; i < 8; ++i)
{
results.emplace_back(
pool.enqueue([i]
{
std::cout << "Task " << i << " started" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Task " << i << " finished" << std::endl;
return i * i; // 返回 i 的平方
})
);
}
// 获取并打印每个任务的结果
for (auto && result : results)
{
// result.get() 是阻塞的
std::cout << "Result: " << result.get() << std::endl;
}
return 0;
}
线程基本概念
线程是操作系统调度的基本单位,它是一个独立的执行路径,与进程共享相同的内存空间和资源。每个线程可以独立执行任务,但在多线程环境下,线程之间可能需要同步或通信,这就带来了上下文切换的开销。
-
线程有自己的函数栈: 每个线程在执行时拥有自己独立的函数栈,用于保存局部变量、返回地址等执行上下文。
-
多线程能够利用CPU的多内核并行执行: 现代处理器通常有多个内核,多个线程可以在不同的内核上同时执行,实现真正的并行处理,从而提高程序的执行效率。
线程池的基本概念
线程池是一种用于管理和复用线程的技术。通过预先创建一定数量的线程来执行任务,而不是每次执行任务时都创建新的线程,从而节省资源并提高性能。
-
线程创建的开销:
-
每次创建线程需要分配函数栈空间(通常默认大小为 1M)以及其他资源。
-
使用线程池可以避免频繁创建和销毁线程的开销,重复利用已有的线程来执行任务。
-
-
线程切换的开销:
-
线程切换涉及上下文切换,包括保存和恢复寄存器状态、切换内核态和用户态等,这些操作都是有开销的。
-
线程池通过控制线程的数量,避免线程过多导致频繁的上下文切换,从而降低系统的开销,提高任务执行的效率。
-
改进方向
线程池的工作原理可以类比成为多批次流水线处理
I/O阻塞可能耗费很长时间,导致线程空闲等待,降低整体的处理效率。
方案一:引入C++20协程 (但是好像协程可以与多进程搭配并发)
思路: 通过C++20的协程功能,将I/O阻塞操作进行挂起处理,当I/O操作完成后再恢复执行。这样可以避免线程在等待I/O操作时被阻塞,进而更高效地利用线程资源。
-
利用
co_await
、co_yield
等协程关键字,将长时间等待的I/O操作挂起。 -
通过编写自定义的协程调度器来管理协程的执行。当I/O操作未完成时,将协程挂起,释放线程的控制权。
优点:
-
高效资源利用:线程不会因为I/O阻塞而闲置,可以继续处理其他任务。
-
简化并发模型:协程提供了类似同步的编程风格,简化了异步I/O的处理逻辑。
缺点:
-
引入复杂性:协程的引入可能需要对现有代码结构进行较大调整。
-
协程上下文切换:虽然轻量,但仍然存在一些开销。
方案二:分离I/O密集和计算密集任务
思路: 将线程池分为两个部分:一个专门处理I/O密集型任务(这些任务可能会因为I/O操作而长时间阻塞),另一个专门处理计算密集型任务(这些任务需要大量CPU计算但不涉及长时间等待)。
-
I/O线程池:专门处理I/O密集型任务。这个线程池中的线程可以被设计为等待I/O完成后再继续处理任务。
-
计算线程池:专门处理计算密集型任务。这个线程池的线程会一直执行CPU密集型任务,不会被I/O操作阻塞。
-
任务在提交到线程池之前,需要根据其特性(I/O密集或计算密集)进行分类,分别提交到不同的线程池中。对于混合型任务(既包含I/O操作,又包含计算任务),可以拆分成两个子任务,分别提交到相应的线程池。
-
I/O线程池同步用
condition_variable
主动沉睡到被唤醒 或std::this_thread::yield()
主动让出时间片等。
... (不太懂,不会。。。)