本文的内容主要参照了陈硕先生编写的muduo网络库,本篇文章源码的地址为:https://github.com/freshman94/NetLib
互斥量
使用RAII的手法封装互斥量,避免因忘记解锁造成的死锁问题。
#include <base/noncopyable.h>
#include <assert.h>
#include <pthread.h>
class MutexLock : noncopyable {
public:
MutexLock() {
pthread_mutex_init(&mutex_, NULL);
}
~MutexLock() {
pthread_mutex_destroy(&mutex_);
}
void lock() {
pthread_mutex_lock(&mutex_);
//holder_ = CurrentThread::tid();
}
void unlock() {
pthread_mutex_unlock(&mutex_);
}
pthread_mutex_t* get() {
return &mutex_;
}
private:
friend class Condition;
pthread_mutex_t mutex_;
};
class MutexLockGuard : noncopyable {
public:
explicit MutexLockGuard(MutexLock& mutex)
: mutex_(mutex) {
mutex_.lock();
}
~MutexLockGuard() {
mutex_.unlock();
}
private:
MutexLock & mutex_;
};
类MutexLock封装了互斥量的初始化和销毁动作,并提供了接口lock和unlock分别执行加锁和解锁操作。实际中使用的应该是类MutexLockGuard,它在在构造时会自动加锁,在作用域会自动调用其析构函数,执行解锁动作。
自定义的条件变量类Condition
pthread库的条件变量太过冗杂,自定义Condition类,只实现所需的功能
class Condition : noncopyable{
public:
explicit Condition(MutexLock& mutex)
: mutex_(mutex)
{
pthread_cond_init(&cond_, NULL);
}
~Condition(){
pthread_cond_destroy(&cond_);
}
void wait(){
pthread_cond_wait(&cond_, mutex_.get());
}
void notify(){
pthread_cond_signal(&cond_);
}
void notifyAll(){
pthread_cond_broadcast(&cond_);
}
private:
MutexLock& mutex_;
pthread_cond_t cond_;
};
使用C++实现的CountDownLatch
CountDownLatch是java实现的一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。在C++中,可以使用互斥量和条件变量来实现相应的功能。
class CountDownLatch : noncopyable{
public:
explicit CountDownLatch(int count);
void wait();
void countDown();
int getCount() const;
private:
mutable MutexLock mutex_;
Condition condition_;
int count_;
};
其成员变量包含一个互斥量和条件变量。count_表示等待的线程数量,用于构造CountDownLatch。关键的成员函数的定义为:
void CountDownLatch::wait(){
MutexLockGuard lock(mutex_);
while (count_ > 0)
condition_.wait();
}
void CountDownLatch::countDown(){
MutexLockGuard lock(mutex_);
--count_;
if (count_ == 0)
condition_.notifyAll();
}
使用gettid()获取线程标识
对于pthread_t类型,glibc的Pthreads实际上把pthread_t用作一个结构体指针。Pthreads只保证同一进程之内,同一时刻的各个线程的id不同;不能保证同一进程先后多个线程具有不同的id,更不要说一台机器上多个进程之间的id唯一性了。因此,pthread_t并不适合作程序中线程的标识符。
建议使用gettid
系统调用的返回值作为线程id,类型为pid_t。pthread_self获得的是POSIX thread ID,gettid是内核中的线程的ID。glibc没有封装这个系统调用,需要自己实现。
pid_t gettid(){
return static_cast<pid_t>(syscall(SYS_gettid));
}
但是每次获取线程ID都执行一次系统调用,似乎有些浪费,陈硕先生设计的muduo库中,采取的方法是用__thread变量来缓存gettid的返回值,这样只有在本线程第一次调用的时候才进行系统调用,以后都是直接从缓存的线程id拿到结果。
inline int tid(){
if (__builtin_expect(t_cachedTid == 0, 0)){
cacheTid();
}
return t_cachedTid;
}
void CurrentThread::cacheTid(){
if (t_cachedTid == 0){
t_cachedTid = gettid();
t_tidStringLength = snprintf(t_tidString, sizeof t_tidString, "%5d ",
t_cachedTid);
}
}
更易使用的线程Thread
muduo库中的Thread类定义为
Thread::Thread(ThreadFunc func, const string& name)
: started_(false),
joined_(false),
pthreadId_(0),
tid_(0),
func_(std::move(func)),
name_(name),
latch_(1)
{
setDefaultName();
}
其中ThreadFunc func是线程的启动函数,name是通过调用prctl函数,给线程定义的名字。
通过调用start函数即可让一个线程启动
void Thread::start(){
assert(!started_);
started_ = true;
ThreadData* data = new ThreadData(func_, name_, &tid_, &latch_);
if (pthread_create(&pthreadId_, NULL, &startThread, data)){
started_ = false;
delete data;
LOG_FATAL << "Failed in pthread_create";
}
//CountDownLatch是为了确保thread的func真正地启动起来,这时start函数才能返回。
else{
latch_.wait();
assert(tid_ > 0);
}
}
void* startThread(void* obj)
{
ThreadData* data = static_cast<ThreadData*>(obj);
data->runInThread();
delete data;
return NULL;
}
void runInThread() {
*tid_ = CurrentThread::tid(); //cache tid
tid_ = NULL;
latch_->countDown();
latch_ = NULL;
CurrentThread::t_threadName = name_.empty() ? "HttpServer_Thread" : name_.c_str();
prctl(PR_SET_NAME, CurrentThread::t_threadName);
func_();
CurrentThread::t_threadName = "finished";
}
其中ThreadData是其定义的一个内部类,用于保存线程的各种信息:线程启动函数、线程名字,线程tid等。这里解释下CountDownLatch类型的变量latch_的作用:为了保证线程函数真正地运行起来之后,start函数才能返回。
对于muduo库实现的Thread类,一般而言,我们会让Thread对象的生命期长于线程,然后通过调用Thread::join()来等待线程结束并释放线程资源。如果Thread对象的生命期短于线程,那么其析构时会自动调用pthread_detach函数来分离线程,避免了资源的泄漏。