类定义
1 | template <typename T> |
从类定义看出,该线程池定义了两个用户 API。
构造函数
1 | template <typename T> |
创建一个线程,这个线程的默认状态是 joinable,意味着线程结束之后需要使用 pthread_join 来显式回收资源,而调用 join 时如果线程还没有运行结束,此时调用函数会被阻塞。线程状态为 detach 的线程运行结束之后自动释放资源。
请求入队
1 | template <typename T> |
- 写请求队列时需要上锁,利用互斥锁进行线程同步,以保证线程安全;
- 写完解锁,并设置信号量
pthread_create
1 |
|
函数原型中的第三个参数,为函数指针,指向处理线程函数的地址。
该函数,要求为静态函数,如果处理线程函数为类成员函数时,需要将其设置为静态成员函数。
2020/7/3 更新
近日打算重构一下服务器代码。
注意到源代码的线程池非单例模式,因此用单例模式和 STL 标准库实现线程池。
从半异步/半反应堆结构的设计出发,由于工作线程需要频繁访问连接资源和任务队列,因此将这两个数据结构一并封装在线程池中,提供一个 API 给主线程(IO线程)插入任务对象(连接 socket)。
而资源类中需要封装请求资源的数据结构,并提供报文解析、响应请求等 API。
单例模式实现过程中遇到如下问题:
invalid use of non-static function
错误原因为之前一直没有注意的:创建新线程时指定的入口函数必须为静态函数,但是如果直接把任务函数声明为静态函数就使得我们必须将任务函数中用到的所有数据成员声明为静态,开销太大,因此需要包装函数,通过get_instance
返回的单例间接调用任务函数
1 | template<typename sockfd, typename resource> |
1 | template<typename sockfd, typename resource> |
如上。
分离 reactor 与 proactor 实现
为将两种事件处理模式分离,在单例模式的基础上加上了类似简单工厂的设计,但问题来了!每种模式的线程池并不知道另一种模式的线程池是否已经存在,因此需要一个接口以供判断。
但是通过在每个类中加一个静态探针成员的办法从设计上来说不太容易实现,因此给工厂加一个限制:只能产生一个实例。
因此把工厂的API分为:获取实例、初始化两种。
但如何返回不同派生类的实例呢?
使用重载函数,用一个模板参数激活函数重载。
使用信号量唤醒线程引起的惊群效应?
可以使用条件变量加互斥锁解决。
先解锁还是先唤醒?
加锁时调用signal
某些平台上,在执行了signal/broadcast之后,为了减少延迟,操作系统会将上下文切换到被唤醒的线程。在单核系统上,如果在加锁的情况下调用signal/broadcast,这可能导致不必要的上下文切换。
考虑上图的场景:T2阻塞在条件变量上,T1在持有锁的情况下调用signal,接着上下文切换到T2,并且T2被唤醒,但是T2在从pthread_cond_wait返回时,需要重新加锁,然而此时锁还在T1手中。因此,T2只能继续阻塞(但是此时是阻塞在锁上),并且上下文又切换回T1。当T1解锁时,T2才得以继续运行。如果是调用broadcast唤醒等待条件变量的多个线程的话,那这种情形会变得更糟。
为了弥补这种缺陷,一些Pthreads的实现采用了一种叫做waitmorphing的优化措施,也就是当锁被持有时,直接将线程从条件变量队列移动到互斥锁队列,而无需上下文切换。
如果使用的Pthreads实现没有waitmorphing,我们可能需要在解锁之后在进行signal/broadcast。解锁操作并不会导致上下文切换到T2,因为T2是在条件变量上阻塞的。当T2被唤醒时,它发现锁已经解开了,从而可以对其加锁。
然而先解锁后唤醒可能会导致伪唤醒,还需要理解。
结论:在没有证明先唤醒后解锁的性能显著低的情况下,先唤醒后解锁。