在分析了muduo的网络库源码之后,觉得muduo网络库关于线程池的实现很有意思,它利用boost库的function和bind这一对神器,摆脱了使用继承和虚函数的束缚,而且摆脱了线程回调函数的特异性,不用拘泥于回调函数的实现形式,我觉得这其中有很多手法值得我来借鉴。
源码下载地址:http://download.csdn.net/detail/yanchen0314/8192959
关于线程池实现的基础:
首先最基础的当然是互斥器的封装,我们使用Linux的互斥锁作为基础,采用RAII手法避免手动的去加锁和释放可能造成的遗漏,值得注意的是互斥器只用非递归的不可重用的互斥锁,非递归是因为防止重复加锁造成的死锁,不可重用是因为防止其发生拷贝而导致不可预期的事情发生,比如,你想操作一个临界区的共享资源,但是你无意中调用了拷贝构造函数对你的互斥器的副本加了锁而自己不知道,这样其实你的资源并没有被上锁,更糟糕的是如果你在解锁的时候不小心调用了互斥器的拷贝构造而对互斥器的副本解了锁,你自己此时以为已经解锁成功了,但其实真正的锁并没有被解开,等到下一次再对它进行加锁操作的时候,就立马导致死锁。让类不可重入的方法可以将类的拷贝构造函数定义成private。或者干脆让它继承自一个不可重入的基类,如boost::noncopyable。
class MutexLock:public boost::noncopyable
{
public:
explicit MutexLock():holder_(0)
{
int ret = pthread_mutex_init(&mutex_,NULL);
assert(ret==0);
}
~MutexLock(void)
{
assert(holder_==0);
pthread_mutex_destroy(&mutex_);
assert(holder_==0);
}
void lock(void)
{
pthread_mutex_lock(&mutex_);
holder_ = CurrentThread::gettid();
}
void unlock(void)
{
pthread_mutex_unlock(&mutex_);
holder_ = 0;
}
pthread_mutex_t *getPthreadMutex(void)
{
return &mutex_;
}
bool isLockByThisThread(void)
{
return holder_ == CurrentThread::gettid();
}
void assertLocked(void)
{
assert(isLockByThisThread());
}
private:
pthread_mutex_t mutex_;
pid_t holder_;
};
class MutexLockGuard:public boost::noncopyable
{
public:
explicit MutexLockGuard(MutexLock &lock)
:mutex_(lock)
{
mutex_.lock(); //使用RAII手法在构造的时候加锁
}
~MutexLockGuard()
{
mutex_.unlock();//使用RAII手法在析构的时候解锁,防止遗漏
}
private:
MutexLock &mutex_;
};
对一个共享数据加锁可以用:
Mutex mutex_;
{
MutexLockGuardlock(mutex_);//注意千万不要将锁嵌套的调用
Count++;
}
class Condition:public boost::noncopyable
{
public:
explicit Condition(MutexLock &lock)
:mutex_(lock)
{
pthread_cond_init(&pcon_,NULL);
}
~Condition()
{
pthread_cond_destroy(&pcon_);
}
void wait(void)
{
pthread_cond_wait(&pcon_,mutex_.getPthreadMutex());
}
void notify(void)
{
pthread_cond_signal(&pcon_);
}
void notifyAll(void)
{
pthread_cond_broadcast(&pcon_);
}
void waitForSeconds(int seconds)
{
struct timespec abstime;
clock_gettime(CLOCK_REALTIME,&abstime);
abstime.tv_sec += seconds;
pthread_cond_timedwait(&pcon_,mutex_.getPthreadMutex(),&abstime);
}
private:
MutexLock &mutex_;
pthread_cond_t pcon_;
};
在对Condition执行wait操作时互斥器必须先加锁
线程的封装,在标准的Linux编程中,线程函数的形式可能必须满足某种特定的格式,如:void *(threadFunction)(void*argv)。但是这样太呆板了,我们能不能使它变得更加多变,变得不拘泥于特定的形式,这似乎是个棘手的问题,直到我看到了boost库的bind和function这对神器,有了他们我们就可以打破格式的限制,不管是非成员函数还是类的成员函数都可以隐式转换成特定的形式。
class CThread:public boost::noncopyable
{
public:
typedef function<void ()> ThreadFunc;
explicit CThread(const ThreadFunc &func,const std::string&name="");
~CThread();
const std::string &name()const{return name_;}
pid_t tid()const{return tid_;}
bool started()const{return started_;}
void start(void);
int join(void);
private:
static void *startThread(void *obj);
void runInThread(void);
pthread_t pthreadID_;
pid_t tid_;
std::string name_;
bool started_;
ThreadFunc func_;
};
CThread在构造函数时指定线程的回调函数
typedef function<void ()> ThreadFunc;
explicit CThread(const ThreadFunc &func,const std::string&name="")
:name_(name)
,func_(func)
,tid_(0)
,pthreadID_(0)
,started_(false)
{
}
再让我们看看start()函数做了什么事:
void CThread::start()
{
assert(!started_);
int err=pthread_create(&pthreadID_,NULL,&startThread,this);
started_ = true;
if(err<0)
{
perror("Thread::start() pthread_create error");
}
}
这创建的线程回调函数是startThread,是CThread的一个私有的成员函数,好像并没有执行构造时指定的回调函数func啊,别急往下看startThread函数做了啥;
void *CThread::startThread(void *obj)
{
CThread *pThread = static_cast<CThread *>(obj);
pThread->runInThread();
return NULL;
}
原来他调用了另一个成员函数runInThread,可是还是没看到startThread的影子啊,接着向下看;
void CThread::runInThread()
{
tid_ = CurrentThread::gettid();
func_();
}
哦,原来真正的调用在这里,这样巧妙的设计就能够达到消除特异性的差异,让我们迫不及待的试一下吧。
void function1(void)
{
printf(“function1\n”);
}
void function(int argc)
{
printf(“function2:%d\n”,argc);
}
Class Test
{
….
public:
void function3(const char *argv)
{
printf(“function3:%s\n”,argv);
}
}
CThread thread1(boost::bind(&function1));
thread1.start();
CThread thread2(boost::bind(&function2,5));
thread2.start();
Test t1;
const char *pData = “Hello”;
CThread thread3(boost::bind(&Test::function3,t1,pData));
thread3.start();
由此可见不管是成员函数还是非成员函数,不管带几个参数,这样的限制都被屏蔽了。
线程池的封装:class ThreadPool
{
public:
typedef boost::function<void ()> Task;
explicit ThreadPool(std::string name="");
~ThreadPool(void);
void start(int numberThreads);
void stop(void);
void run(const Task &f);
private:
void runInThread(void);
Task take(void);
MutexLock mutex_;
Condition cond_;
std::string name_;
boost::ptr_vector<CThread> threads_;
std::deque<Task> queue_;
bool runing_;
};
void ThreadPool::start(int numberThreads)
{
assert(threads_.empty());
runing_ = true;
threads_.reserve(numberThreads);
for(int i=0;i<numberThreads;++i)
{
char id[32] ={0};
snprintf(id,31,"%d",i);
threads_.push_back(new CThread(boost::bind(&ThreadPool::runInThread,this),name_+id));
threads_[i].start();
}
}
在这里指定你创建的线程池中有几个线程,每个线程的回调函数都去执行runInThread;那么runInThread到底在干什么呢?
void ThreadPool::runInThread()
{//这里才是线程真正执行的函数
while(runing_)
{//线程不断的在循环的等待任务,这样线程在不调用stop的情况下就不会退出
Task task(take());//每个线程在这里等待,等待到后就执行
if(task)
{
task();
}
}
}
它似乎是去获取一个任务后然后去执行,那我们来看看任务是怎么获取的。
ThreadPool::Task ThreadPool::take()
{
MutexLockGuard lock(mutex_);
if(queue_.empty() && runing_)
{
cond_.wait();//在没有任务的时候,就在这里阻塞
}
Task task;
if(!queue_.empty())
{
task = queue_.front();
queue_.pop_front();
}
return task;
}
看明白了吧,这里有一个条件变量来负责等待任务,如果人祸列表为空就等待,一直等待有任务到来为止,那任务什么时候到来,到来的又是什么样的任务呢,别急,我们往下面看。
void ThreadPool::run(const ThreadPool::Task &f)
{//往任务列表中去添加任务,唤醒阻塞的线程去执行
if(threads_.empty())
{
f();
}
else
{
MutexLockGuard lock(mutex_);
queue_.push_back(f);
cond_.notify();
}
到这里疑惑都解开了吧,当用户执行run的时候,指定了线程回调函数f,如果线程池里没有一个线程,那么f()就直接在主线程里执行了,如果有其他的线程在等待,那么将任务加入到任务执行列表,然后通知所有正在等待的线程,有任务来了,线程们一收到通知立刻唤醒去抢占任务。在这里就都明白了吧。
为什么要这样封装线程池?
1.其中用到RAII手段,可以防止你手动加锁导致的一系列的麻烦。
2.可以消除线程回调函数的特异性,接口不会受到形式的拘泥
3.使用Linux自带的条件变量进行抢占式执行,能够使程序有足够的效率去响应任务
4.随时随地控制线程的数量,能够方便将你的程序更加充分的利用现有的硬件资源
我能想到的好处就这些了,至于其他的妙用希望读者自己去发掘。