当前位置 博文首页 > wanggao的专栏:muduo学习笔记:base部分之CurrentThread命名空
muduo的Thread是pthread的封装,实现了线程的基本操作,CurrentThread命名空间实现了有关线程id的管理和优化。
主要用于获取当前线程的id,并将线程id保存为C语言风格的字符串。这里使用几个优化处理的tricks,下面面分别说明
namespace CurrentThread
{
// internal
extern __thread int t_cachedTid;
extern __thread char t_tidString[32];
extern __thread int t_tidStringLength;
extern __thread const char* t_threadName;
void cacheTid();
inline int tid(){
if (__builtin_expect(t_cachedTid == 0, 0)){
cacheTid();
}
return t_cachedTid;
}
inline const char* tidString(){ return t_tidString; } // for logging
inline int tidStringLength() { return t_tidStringLength; } // for logging
inline const char* name() { return t_threadName; }
bool isMainThread() { return tid() == ::getpid(); }
void sleepUsec(int64_t usec); // only for testing
string stackTrace(bool demangle); // trace使用
}
__thread是GCC内置的线程局部存储设施,__thread变量每一个线程有一份独立实体,各个线程的值互不干扰。可以用来修饰那些带有全局性且值可能变,但是又不值得用全局变量保护的变量。
简单来说,就是每个用该修饰符修饰的变量在每个线程中都会有存在,例如这里的每个线程都一个id标识符__thread int t_cachedTid
,都有一个对应的id字符串__thread char t_tidString[32]
,字符串的长度__thread int t_tidStringLength
。
这里使用__thread变量缓存了线程id、id字符串用于日志中,避免可能每次输出log时都需要获取一次线程id(一次系统调用)、再转换成字符串的操作,从而提高了效率。
我们所说的分支预测是指,高级编程语言在编译成汇编指令后,由于cpu进行流水线式的执行,汇编过程中将多个条件判断分支按需进行优化,最近的条件语句执行效率最高,其他的需要进行jmp跳转。因jmp跳转效率相对较低,因此我们在高级语言编程需要将最可能发生的条件判断分支写在最前侧。
函数 int tid()
目的是获取线程的id,若第一次获取,则缓存到__thread t_cachedTid变量中,后续获取则直接返回该缓存值。很明显,这里仅第一次满足if条件,后面使用将不再执行if满足的代码块。判断语句if( __builtin_expect(t_cachedTid == 0, 0) )
就是说明逻辑判断 t_cachedTid == 0
多数情况为0(假),进行了汇编层次的优化。
函数 void cacheTid()
的实现位于Thread.cc文件中,内部调用系统调用::syscall(SYS_gettid)
返回值作为线程id,并将线程id保存了C语言风格字符及其长度。
namespace detail
{
pid_t gettid() {
return static_cast<pid_t>(::syscall(SYS_gettid));
}
} // namespace detail
void CurrentThread::cacheTid()
{
if (t_cachedTid == 0)
{
t_cachedTid = detail::gettid();
t_tidStringLength = snprintf(t_tidString, sizeof t_tidString, "%5d ", t_cachedTid);
}
}
为什么使用gettid系统调用返回值作为线程id标识?
POSIX threads库提供了pthread_self函数返回值作为线程标识符,类型是pthread_t。该类型存在一些问题:
(1)在不同平台pthread_t可能是一个数值、也可能是一个结构体,快平台处理麻烦;
(2)不好比较大小,无法计算hash值不能作为关联容器的key
(3)不能定义一个非法pthread_t值来标识一个不存在的线程id
(4)只在进程内有意义,系统内任务调度间无关联。如在/proc中找不到pthread_t对应任务
(5)glibc中是实现是一个long int值,保证在统一进程中,同一时刻不同线程具有不同id,而不能保证不同时刻多个线程有不同id。例如一个线程销毁后,再创建可能出现相同的id。
综上:pthread_t不适合作为线程id的标识,而使用gettid()系统调用返回值作为线程的唯一标识符(使用 man gettid查看详细信息)。好处有:
(1)类型是pid_t,小整数,标语日志保存
(2)标识内核任务调度id,可以在/proc/tid或/proc/pid/task/tid找到对应项
(3)任何时候全局唯一,短时间内反复创建多个线程不会出现相同id(递增到上限值,从头再开始分配)
(4)0是非法制,操作系统第一进程init的id是1
(5)在主线程中,系统调用::getpid()和gettid()相同,用于确认当前线程是否为主线程。
由于glibc中并没有直接实现该函数,需要自己封装,因此这里就使用syscall进行调用获取,并且进行了缓冲,避免内核调用效率的问题。另外/proc/sys/kernel/pid_max和/proc/sys/kernel/threads-max默认情况都是32768。
首先,在前面CurrentThread还有一个__thread const char* t_threadName
变量为介绍。该变量用来标记当前当前线程的名称,结合以thread.cc文件中码说明:
void afterFork()
{
muduo::CurrentThread::t_cachedTid = 0;
muduo::CurrentThread::t_threadName = "main";
CurrentThread::tid();
// no need to call pthread_atfork(NULL, NULL, &afterFork);
}
class ThreadNameInitializer
{
public:
ThreadNameInitializer()
{
muduo::CurrentThread::t_threadName = "main";
CurrentThread::tid();
pthread_atfork(NULL, NULL, &afterFork);
}
};
ThreadNameInitializer init;
定义了一个全局的ThreadNameInitializer init
对象,当程序运行时,进行init的构造,当前线程为主线程。
在构造函数中,线程名称赋值为“main”,调用tid()函数获取线程id及其id字符串;另外注册了fork进程时的处理函数,可以简单认为是避免父、子进程的id相同。
下面介绍Thread类
Thread类是对pthread库的高级封装,构造时传入回调函数和线程名称,主要函数有:
(1)函数start():内部使用pthread_create()
创建线程,使用CountDownLatch
来等到回调函数进入初始化成功;
(2)函数join():阻塞地等待回调函数执行结束;
(3)函数started():判断线程是否正在执行中;
(4)函数tid():返回当前线程tid;
(5)静态函数numCreated():当前已经创建的线程数量,用于线程默认名称;
(6)函数setDefaultName():设置线程默认名称,“Thread_” + 当前创建线程数量。
后续对其中几个函数做详细说明。
class Thread : noncopyable
{
public:
typedef std::function<void ()> ThreadFunc;
explicit Thread(ThreadFunc, const string& name = string());
// FIXME: make it movable in C++11
~Thread();
void start();
int join(); // return pthread_join()
bool started() const { return started_; }
// pthread_t pthreadId() const { return pthreadId_; }
pid_t tid() const { return tid_; }
const string& name() const { return name_; }
static int numCreated() { return numCreated_.get(); }
private:
void setDefaultName(); // 默认线程名称 Thread_num
bool started_; // 线程是否开始运行
bool joined_; // 线程是否可join
pthread_t pthreadId_; // 线程变量
pid_t tid_; // 线程tid
ThreadFunc func_; // 线程回调函数
string name_; // 线程名称
CountDownLatch latch_; // 用于等待线程函数执行完毕
static AtomicInt32 numCreated_; // 原子操作,当前已经创建线程的数量
};
传递一个线程实际执行的回到函数func、线程名称(默认为空)。主要对数据成员进行初始化,还没创建线程执行入口函数。
Thread::Thread(ThreadFunc func, const string& n)
: started_(false),
joined_(false),
pthreadId_(0),
tid_(0),
func_(std::move(func)),
name_(n),
latch_(1)
{
setDefaultName();
}
void Thread::setDefaultName()
{
int num = numCreated_.incrementAndGet();
if (name_.empty())
{
char buf[32];
snprintf(buf, sizeof buf, "Thread%d", num);
name_ = buf;
}
}
将数据封装在ThreadData
类,执行pthread_create
创建线程。实际执行过程在ThreadData
类的runInThread()
成员函数中。
void Thread::start()
{
assert(!started_);
started_ = true;
// FIXME: move(func_)
detail::ThreadData* data = new detail::ThreadData(func_, name_, &tid_, &latch_);
if (pthread_create(&pthreadId_, NULL, &detail::startThread, data))
{
started_ = false;
delete data; // or no delete?
LOG_SYSFATAL << "Failed in pthread_create";
}
else
{
latch_.wait(); //主线程等待子线程初始化完毕才开始工作,在runInThread()中
assert(tid_ > 0);
}
}
使用了CountDownLatch
类,这个类主要成员是计数值和条件变量,当计数值为0,条件变量发出通知。既可以用于所有子线程等待主线程发起 “起跑” ;也可以用于主线程等待子线程初始化完毕才开始工作。
struct ThreadData
{
typedef muduo::Thread::ThreadFunc ThreadFunc;
ThreadFunc func_;
string name_;
pid_t* tid_;
CountDownLatch* latch_;
ThreadData(ThreadFunc func, const string& name, pid_t* tid, CountDownLatch* latch)
: func_(std::move(func)), name_(name), tid_(tid), latch_(latch)
{ }
void runInThread()
{
*tid_ = muduo::CurrentThread::tid();
tid_ = NULL;
latch_->countDown