Poco ThreadPool 源码剖析

632 阅读6分钟

Poco ThreadPool 源码剖析

版本:poco-1.13.3

Poco::ThreadPool 类

先看它提供了哪些接口

class ThreadPool
{
public:
	// 支持设置线程池名称,线程数量区间,线程可空闲时长等
	ThreadPool(const std::string& name,
		int minCapacity = 2,
		int maxCapacity = 16,
		int idleTime = 60,
		int stackSize = POCO_THREAD_STACK_SIZE);

	// 增加线程最大值
	void addCapacity(int n);
	int capacity() const;

	// 线程堆栈大小
	void setStackSize(int stackSize);
	int getStackSize() const;

	// 正在忙碌中的线程数量
	int used() const;

	// 目前池中已创建多少个线程了
	int allocated() const;

	// 还剩下多少个线程可用
	int available() const;

	// 启动任务
	// 并没有队列,一旦线程池中没有空闲线程,便会失败抛异常
	// 注意:线程池并不管理Runnable生命周期,需要外部自己维护
	//   意味着在任务未结束之前,Runnable对象绝对不能被释放
	void start(Runnable& target);
	void start(Runnable& target, const std::string& name);
	void startWithPriority(Thread::Priority priority, Runnable& target);
	void startWithPriority(Thread::Priority priority, Runnable& target, const std::string& name);

	// 这应该是线程池在销毁前调用的最后一个函数
	// 它会销毁所有的线程(注意:如果线程没能在10秒内正常退出,线程池将不再管理它,它将变成一个野线程)
	void stopAll();

	// 等待所有线程完成手头任务(注意:不是线程退出)
	// 同时清理掉已过期线程
	void joinAll();

	// 清理已过期线程
	void collect();

	// 线程池名称
	const std::string& name() const;

	// 单例
	static ThreadPool& defaultPool();
    
private:
	std::string _name; // 线程池名称
	int _minCapacity; // 最小线程数
	int _maxCapacity; // 最大线程数
	int _idleTime; // 最大空闲时长,单位:秒
	int _serial; // 从0开始自增,用于拼接到池中每个线程的名称中去,区分开各个线程用
	int _age; // 从0开始自增,每次调用start系列接口它都会自增1,自增到32时会调用一次线程池清理
	int _stackSize; // 线程堆栈大小
	std::vector<PooledThread*> _threads; // 所有的线程句柄
	mutable FastMutex _mutex; 
};

通过上述接口,我们可以看出:

  1. Poco::ThreadPool 没有任务队列
  2. 不支持取消任务
  3. 线程池的清理动作,是在添加任务时触发

PooledThread 类

本质就是后台线程,在死循环中不断等待线程池给它分配任务,直接上代码:

class Runnable
{
public:
	Runnable();
	virtual ~Runnable();
	virtual void run() = 0;
};

class PooledThread: public Runnable
{
public:
	PooledThread(const std::string& name, int stackSize = POCO_THREAD_STACK_SIZE);

    // 用于创建线程
	void start();
    
    // 给成员变量_pTarget赋值,并唤醒线程起来工作
	void start(Thread::Priority priority, Runnable& target);
	void start(Thread::Priority priority, Runnable& target, const std::string& name);
    
	bool idle(); // 是否空闲
	int idleTime(); // 已经空闲多久了
	void join(); // 等待线程完成任务(不是退出)
	void activate(); // 将线程置为非空闲状态
	void release(); // 通知线程正常退出,10秒内正常退出后会销毁当前类对象,否则不会执行销毁操作
	void run(); // 线程入口函数

private:
	volatile bool        _idle; // 是否空闲
	volatile std::time_t _idleTime; // 空闲时的打卡时间,后面会用当前时间减去这个打卡时间来计算是不是已经到过期时间了
	Runnable*            _pTarget; // 外部用户传入的真正要执行的任务接口
	std::string          _name; // 线程名称
	Thread               _thread; // 这是Poco对线程的封装,可以理解为std::thread,用于创建线程,不再细说
	Event                _targetReady; // 外部用户传入的任务_pTarget被赋值可用后触发
	Event                _targetCompleted; // 任务完成后触发
	Event                _started; // 线程跑进入口函数run后会第一时间触发该事件
	FastMutex            _mutex;
};

这里再重点看一下它的 run 函数:

void PooledThread::run()
{
	_started.set();
	for (;;)
	{
		_targetReady.wait();
		_mutex.lock();
		if (_pTarget) // 如果目标任务为空,线程会进入退出流程
		{
			Runnable* pTarget = _pTarget;
			_mutex.unlock();
			try
			{
				pTarget->run(); // 执行真正的用户任务
			}
			catch (Exception& exc)
			{
				ErrorHandler::handle(exc);
			}
			catch (std::exception& exc)
			{
				ErrorHandler::handle(exc);
			}
			catch (...)
			{
				ErrorHandler::handle();
			}
            
			FastMutex::ScopedLock lock(_mutex);
			_pTarget  = 0;
			_idleTime = time(NULL); // 空闲下来后,第一时间打卡记录
			_idle     = true; // 标识空闲状态
			_targetCompleted.set(); // 标识任务已完成
			ThreadLocalStorage::clear();
			_thread.setName(_name);
			_thread.setPriority(Thread::PRIO_NORMAL);
		}
		else
		{
			_mutex.unlock();
			break;
		}
	}
}

ThreadPool::start 函数

了解完相关类的组成结构,下面来看下任务的创建流程,直接上代码:

// 新建一个线程对象
PooledThread* ThreadPool::createThread()
{
	std::ostringstream name;
	name << _name << "[#" << ++_serial << "]"; // 线程名称用一个自增量来控制其唯一性
	return new PooledThread(name.str(), _stackSize);
}

// 从池中拿一个空闲线程出来
PooledThread* ThreadPool::getThread()
{
	FastMutex::ScopedLock lock(_mutex);

	if (++_age == 32) // 每添加到第32个任务,触发一次线程池清理
		housekeep(); // 线程池清理函数,会清理掉已过期的线程

	PooledThread* pThread = 0;
	for (ThreadVec::iterator it = _threads.begin(); !pThread && it != _threads.end(); ++it)
	{
		if ((*it)->idle())
			pThread = *it; // 如果有空闲线程,直接取出来用
	}
    
    // 没有空闲线程
    //   则判断下有没有达到最大限制
    //   如果没有超限制,则新建一个线程
    //   否则抛异常
	if (!pThread)
	{
		if (_threads.size() < _maxCapacity)
		{
			pThread = createThread();
			try
			{
				pThread->start(); // 启动线程,进入等待状态,等待被唤醒执行任务
				_threads.push_back(pThread);
			} catch (...)
			{
				delete pThread;
				throw;
			}
		}
		else
			throw NoThreadAvailableException();
	}
    
	pThread->activate();
	return pThread;
}

void ThreadPool::start(Runnable& target)
{
	getThread()->start(Thread::PRIO_NORMAL, target);
}

void ThreadPool::start(Runnable& target, const std::string& name)
{
	getThread()->start(Thread::PRIO_NORMAL, target, name);
}

void ThreadPool::startWithPriority(Thread::Priority priority, Runnable& target)
{
	getThread()->start(priority, target);
}

void ThreadPool::startWithPriority(Thread::Priority priority, Runnable& target, const std::string& name)
{
	getThread()->start(priority, target, name);
}

ThreadPool::joinAll 函数

等待所有任务完成

void ThreadPool::joinAll()
{
	FastMutex::ScopedLock lock(_mutex);

	for (auto pThread: _threads)
	{
		pThread->join();
	}
	housekeep(); // 线程池清理函数,会清理掉已过期的线程
}

ThreadPool::stopAll 函数

它与 joinAll 不同之处在于,默认每个线程只等10秒,超时会,会丢掉该线程,使其变成野线程

void ThreadPool::stopAll()
{
	FastMutex::ScopedLock lock(_mutex);

	for (auto pThread: _threads)
	{
		pThread->release();
	}
	_threads.clear();
}

ThreadPool::housekeep 函数

看下它是如何进行线程清理的:


void ThreadPool::housekeep()
{
	_age = 0; // 计数器清零(每累加到32时触发一次本函数调用

	if (_threads.size() <= _minCapacity)
		return; // 线程数量少于最小值,无需清理

	ThreadVec idleThreads;
	ThreadVec expiredThreads;
	ThreadVec activeThreads;
	idleThreads.reserve(_threads.size());
	activeThreads.reserve(_threads.size());

	// 遍历全部线程,分离出空闲线程、过期线程、活动线程
	for (auto pThread: _threads)
	{
		if (pThread->idle())
		{
			if (pThread->idleTime() < _idleTime)
				idleThreads.push_back(pThread); // 空闲还没超时的放到空闲里
			else
				expiredThreads.push_back(pThread); // 空闲超时的放到过期里
		}
		else activeThreads.push_back(pThread); // 非空闲的放到活动线程里
	}
        
	// 下方代码的一堆操作,主要作用:
	//   1. 把线程分为两波:活动线程和非活动线程
	//   2. 将非活动线程排到池中管理的线程列表最前面,然后紧跟活动线程
	//   3. 已过期的线程清理时有个策略:
	//      a) 所有线程数量加起来,达不到最小线程数,已过期的线程不会被清理掉
	//      b) 若已到达最小线程数,多出来的线程,如果是已过期线程才会被清算出局
	//      c) 若已到达最小线程数,但出来的线程还没过期,不会被清理
	//      
	int n = (int) activeThreads.size();
	int limit = (int) idleThreads.size() + n;
	if (limit < _minCapacity) limit = _minCapacity;
	idleThreads.insert(idleThreads.end(), expiredThreads.begin(), expiredThreads.end());
	_threads.clear(); // 清空掉池中维护的所有线程句柄
	for (auto pIdle: idleThreads)
	{
		if (n < limit)
		{
			_threads.push_back(pIdle);
			++n;
		}
		else pIdle->release();
	}
        
	// 把活动线程追加到列表尾部
	_threads.insert(_threads.end(), activeThreads.begin(), activeThreads.end());
}

总结

Poco::ThreadPool 是全面支持多线程调用的,也支持动态线程数量管理,但是当有线程空闲时间太久,需要被清理时,它并不会立即清理,而是当添加到每32个任务后,才有可能触发一次清理动作。也就是说:如果你的任务数量达不到32个,线程池可能永远也不会清理已过期的线程。

不支持任务队列,一旦没有空闲线程,任务就无法再添加进去。

不支持取消已经添加的任务。

不管理 Runnable 对象生命周期,需要调用者自己管理。