线程池、任务队列及锁机制优化
- 线程池:
- 原理:预先创建一定数量的线程,这些线程在创建后不会轻易销毁,而是持续等待任务。当有新任务到来时,线程池中的线程从任务队列中取出任务并执行,执行完毕后再等待下一个任务。这样避免了频繁创建和销毁线程带来的开销。
- 实现:使用
std::thread
创建线程,将线程对象存储在std::vector
中。例如:
std::vector<std::thread> threads;
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait(lock, [this] { return stop_ ||!tasks_.empty(); });
if (stop_ && tasks_.empty())
return;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
- 任务队列:
- 原理:作为线程池和外部任务提交者之间的桥梁,用于存储待执行的任务。任务以
std::function
或自定义任务结构体的形式放入队列。
- 实现:使用
std::queue
或std::deque
存储任务,同时配合std::mutex
和std::condition_variable
来实现线程安全的访问。例如:
std::queue<std::function<void()>> tasks_;
std::mutex mutex_;
std::condition_variable condition_;
void enqueue(std::function<void()> task) {
{
std::unique_lock<std::mutex> lock(mutex_);
tasks_.push(std::move(task));
}
condition_.notify_one();
}
- 锁机制:
- 减少锁竞争:
- 细粒度锁:将大的锁范围拆分成多个小的锁,每个锁保护不同的资源或数据结构。例如,对于不同的任务队列分区或不同的共享数据块使用不同的锁。
- 读写锁:如果对共享数据的操作大多是读操作,可以使用读写锁(
std::shared_mutex
)。读操作时允许多个线程同时获取读锁,写操作时获取写锁,写锁独占。
- 无锁数据结构:对于一些简单的数据结构,如栈或队列,可以使用无锁数据结构(如
std::atomic
构建的无锁队列),避免锁带来的开销。
不同网络IO模型下多线程应用的不同点
- select:
- 特点:支持的文件描述符数量有限(通常为1024),每次调用
select
都需要将所有的文件描述符集合从用户态拷贝到内核态,并且返回后需要遍历整个集合来检查哪些文件描述符就绪。
- 多线程应用:多线程在这种模型下,每个线程可以负责处理一部分文件描述符集合,减少单个线程的处理压力。但由于频繁的内核态与用户态数据拷贝和遍历操作,线程间的协作开销较大。如果线程数量过多,会导致上下文切换频繁,性能反而下降。
- poll:
- 特点:与
select
类似,但没有文件描述符数量的限制。同样需要将文件描述符集合从用户态拷贝到内核态,返回后也需要遍历集合检查就绪情况。
- 多线程应用:多线程可以按照不同的业务逻辑或连接类型来分配文件描述符的处理。然而,由于其实现机制与
select
相似,在高并发场景下,随着线程数量增加,内核态与用户态数据拷贝和遍历的开销仍然会成为性能瓶颈。
- epoll:
- 特点:采用事件驱动机制,通过
epoll_ctl
注册文件描述符,内核会在文件描述符就绪时将其放入一个就绪列表,epoll_wait
只需要从这个列表中获取就绪的文件描述符,无需遍历整个集合,并且只在注册时进行一次内核态与用户态数据拷贝。
- 多线程应用:在多线程场景下,可以将
epoll
实例分配给不同的线程,每个线程处理自己epoll
实例上的就绪事件。这种方式可以充分利用多核CPU的优势,减少线程间的竞争和上下文切换开销,在高并发场景下性能表现较好。但需要注意线程安全问题,例如在注册和注销文件描述符时要保证线程安全。