什么是线程池?
线程池是一种管理和复用线程的机制,它可以在需要执行任务时分配线程,执行完任务后将线程返回给线程池以供重用。线程池的主要目的是优化线程的创建和销毁过程,以提高应用程序的性能和资源利用率。
线程池中包含一组预先创建的线程,这些线程在池中等待被分配任务。当有任务到达时,线程池会选择一个空闲的线程来执行任务,或者在没有空闲线程时创建一个新的线程。执行完任务后,线程可以被重新放入线程池中,以便执行其他任务,而不是销毁该线程。
线程池的优势包括:
-
降低线程创建和销毁的开销:线程的创建和销毁是一项开销较大的操作。使用线程池可以减少这些开销,通过重用现有线程来执行任务,避免频繁地创建和销毁线程。
-
控制并发线程的数量:线程池可以限制同时执行的线程数量,防止线程过多导致系统资源耗尽。通过控制线程数量,可以更好地管理系统的并发性和负载。
-
提供线程管理和监控:线程池提供了一些功能来管理和监控线程的状态、执行情况和异常处理。可以设置线程的优先级、超时时间和异常处理策略等。
-
提高系统响应性和吞吐量:使用线程池可以提高系统的响应性能和任务处理的吞吐量。通过并发执行多个任务,可以更有效地利用系统资源,提高任务处理的效率。
总之,线程池是一种重要的并发编程工具,能够管理线程的生命周期和执行任务,提高系统的性能和资源利用率。在多线程编程中,合理使用线程池可以帮助我们更好地控制并发,并简化线程管理的复杂性。
线程池的使用过程
-
创建线程池:使用编程语言或框架提供的API,创建一个线程池对象。在创建线程池时,需要指定线程池的参数,例如线程池的大小、最大线程数、任务队列的大小等。
-
提交任务:将需要执行的任务提交给线程池。任务可以是实现了某个接口或继承了某个类的对象,也可以是一个函数或方法。线程池会将任务添加到任务队列中,并等待空闲线程来执行任务。
-
执行任务:线程池会自动从任务队列中获取任务,并将任务分配给空闲的线程来执行。线程池会根据设定的策略来选择线程执行任务,例如先进先出、优先级等。执行任务的线程会调用任务对象的执行方法或执行函数来完成任务。
-
处理结果:如果任务执行完成后产生了结果,可以通过回调函数、Future对象或其他方式来获取任务的结果。线程池可以提供相应的机制来获取任务的执行结果,以便进一步处理或返回给调用者。
-
关闭线程池:在不需要继续使用线程池时,应该及时关闭线程池,释放资源。关闭线程池时,线程池会停止接受新任务,并等待已提交的任务执行完成。可以选择等待所有任务执行完成后再关闭线程池,或者强制立即关闭线程池。
在使用线程池时,需要根据具体的需求来配置线程池的参数,例如线程池的大小、任务队列的容量、线程的生命周期等。合理配置线程池的参数可以充分利用系统资源,提高程序的并发性能和响应能力。
同时,需要注意线程池的线程安全性和同步问题。在多线程环境下,共享资源的访问需要进行同步或互斥操作,以避免资源竞争和数据不一致的问题。可以使用线程安全的数据结构或加锁机制来保证线程安全性。
最后,应该根据具体的业务需求和系统负载情况来评估和调整线程池的大小和参数,以达到最优的性能和资源利用率。
线程池的适用过程
线程池适用于许多并发编程场景,特别是在需要频繁执行短期任务的情况下。以下是线程池适用的一些常见场景:
-
Web服务器:在Web服务器中,每个请求都需要在后台执行一些任务,例如处理请求、查询数据库、生成响应等。使用线程池可以管理并发请求,避免为每个请求创建和销毁线程,提高服务器的并发性能。
-
并发任务处理:当需要并发地执行大量任务时,线程池可以帮助有效地管理线程并提高任务处理的吞吐量。例如,批量处理数据、并行计算、图像处理等场景都可以使用线程池来加速任务的执行。
-
定时任务调度:线程池可以用于执行定时任务,例如在指定的时间间隔内定期执行某个任务。通过线程池,可以实现高效的定时任务调度,避免频繁地创建和销毁线程。
-
异步事件处理:当需要处理异步事件或消息时,线程池可以帮助并发地处理事件并提高响应性能。例如,消息队列、事件驱动编程、异步通信等场景都可以使用线程池来处理事件或消息的触发和处理过程。
-
并发资源访问:在多线程环境下,对共享资源的访问需要进行同步或互斥操作。线程池可以帮助管理对共享资源的并发访问,避免资源竞争和死锁等问题。
-
数据库连接池:在数据库访问中,每个连接的创建和销毁都是比较耗时的操作。通过使用线程池管理数据库连接,可以避免频繁地创建和销毁连接,提高数据库访问的效率。
需要注意的是,线程池并不适用于所有情况。在某些特定的场景下,例如需要精确控制线程的生命周期、线程间需要进行通信或同步等情况下,可能需要手动创建和管理线程。此外,使用线程池时需要根据实际情况合理配置线程池的大小、线程数、任务队列等参数,以充分发挥线程池的优势。
现在让我们进入代码了解一下:
任务队列:
默认先来的任务先处理,因此队列是比较合适的数据结构,但是由于是多线程,线程池和任务队里之间的关系是典型的生产者-消费者模型,当多个线程试图查询任务队列并取走任务执行时,会有可能导致同一个任务被多个线程执行,因此我们需要一把锁mutex来限制并发访问。
template <typename T>
class task_queue
{
private:
std::queue<T> m_queue; //存放任务的队列
std::mutex m_mutex; //互斥锁保证安全的并发访问
public:
task_queue(){}
task_queue(task_queue &&other){}
~task_queue(){}
bool isEmpty()
{
std::unique_lock<std::mutex> lock(m_mutex);
return m_queue.empty();
}
int size()
{
std::unique_lock<std::mutex> lock(m_mutex);
return m_queue.size();
}
void enqueue(T &t) //向任务队列添加元素
{
std::unique_lock<std::mutex> lock(m_mutex);
m_queue.emplace(t);
}
bool dequeue(T &t) //从任务队列取出元素
{
std::unique_lock<std::mutex> lock(m_mutex);
if(m_queue.empty())
return false;
t=std::move(m_queue.front()); //std::move() 将队列中的元素进行右值引用,表示该元素的所有权可以被移动或转移给其他对象,而不是进行常规的拷贝操作。
m_queue.pop();
return true;
}
};
1、先说一下task_queue(task_queue &&other){}这行代码,这是移动构造函数。移动构造函数的目的是获取 other
中的资源,并将其转移到当前对象中,同时使 other
进入有效但未定义的状态。这样做可以避免不必要的资源复制和内存分配,提高性能。
2、上面的代码看起来好像只有上锁的操作而没有解锁的操作,实际并非如此。std::unique_lock
是一个 RAII(资源获取即初始化)风格的类,用于管理互斥量的加锁和解锁。它在构造时获取互斥量的锁,并在析构时释放锁。这样可以确保在任何情况下,即使函数发生异常或提前返回,锁也能被正确释放,从而避免死锁和资源泄漏。在上面代码中,std::unique_lock<std::mutex> lock(m_mutex);
会在函数内部创建一个 lock
对象,并自动获取互斥量的锁。当 lock
对象超出作用域时,即函数结束时,它的析构函数会自动释放互斥量的锁。这样就保证了互斥量在合适的时机被正确释放,避免了死锁。
工作队列:
工作队列的任务是执行线程池交付给它的任务。C++11完成的线程池详情请参见基于C++11实现线程池 - 知乎
这篇文章。里面对一些语法糖讲解的很好。
在线程池的构造函数中,会创建指定数量的工作线程,每个工作线程都是通过调用 std::thread(ThreadWorker(this, i))
创建的,其中 ThreadWorker
是一个可调用对象,表示工作线程的函数。
在 ThreadWorker
对象的 operator()
中,线程会循环地从任务队列中取出函数对象并执行,直到线程池的 m_shutdown
标志位为 true,或者任务队列为空。在取出函数对象之前,线程会通过互斥锁和条件变量进行等待,当有任务加入到队列中时,会唤醒一个等待的工作线程。
使用线程池时,可以通过调用 submit()
函数提交一个函数到线程池中执行。函数会被封装成一个函数对象,并放入任务队列中等待执行。同时,会通过条件变量唤醒一个等待的工作线程来执行任务,并返回一个 std::future
对象,可以用于获取函数执行的结果。