muduo笔记 线程类Thread,当前线程CurrentThread
线程类Thread要解决的问题
从用户角度,一个线程类应该要提供什么给用户?
线程类最核心的内容显然是为用户提供另一个执行流,让用户程序能以线程方式并发执行(调用线程与新线程“同时”执行),但同时能共享同一个进程的内存空间。同时,作为用户,我们希望能对这个线程设置用户提供的线程函数,还有对线程进行控制,包括启动、停止、回收资源(连接);获得这个线程在内核或线程库中的线程id,是否已启动、是否已连接(被回收资源)等状态信息。为了方便调试、打印/查看log,我们可能还需要为线程设置标识,如用户指定的线程id和线程名称等信息。
现有的线程能提供什么?
Linux下,C++ 11 std::thread 也是用NPTL提供的pthreads实现的,因此,我们主要考虑pthreads。
pthreads主要接口:
#include <pthread.h>
// 创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
// 连接线程
int pthread_join(pthread_t thread, void **retval);
// 分离线程, 线程分离后, 调用线程无需其他线程join
int pthread_detach(pthread_t thread);
// 退出调用线程,
void pthread_exit(void *retval);
// 取消(指定)线程
int pthread_cancel(pthread_t thread);
// 判断2个线程id是否相同
int pthread_equal(pthread_t t1, pthread_t t2);
注:以上线程函数的使用,都需要用-pthread编译、链接。
封装线程类Thread
根据pthreads接口pthread_*,Thread要实现:
- 基本线程的原语:线程的创建和等待结束。
- 线程控制的状态:是否已经创建(启动),是否已经结束(连接)。
- 线程属性:线程id,线程名称。
- 线程统计信息:通过Thread class创建的线程数量。
线程类的拷贝没有实际意义,因为线程会对应内核中的数据结构,运行状态等。
Thread 接口
因此,我们可以为Thread设计如下接口:
class Thread : noncopyable
{
public:
typedef std::function<void()> ThreadFunc;
explicit Thread(ThreadFunc, const string& nameArg = string());
~Thread();
void start();
int 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();
bool started_; // 启动状态
bool joined_; // 连接状态
pthread_t pthreadId_; // 用来绑定NPTL线程
pid_t tid_; // 当前线程tid, 通过CurrentThread::tid()获取
ThreadFunc func_; // 用户设置的线程函数
string name_; // 用户自定义名称, 用于debug, log
CountDownLatch latch_; // 向下计数器, 用于同步调用线程和新线程
static AtomicInt32 numCreated_; // 原子类型, Thread class已经创建的线程数量
};
Thread 实现
Thread对象构造,决定了数据成员的初始化
AtomicInt32 Thread::numCreated_;
Thread::Thread(Thread::ThreadFunc func, const string& nameArg)
: started_(false),
joined_(false),
pthreadId_(0),
tid_(0),
func_(std::move(func)),
name_(nameArg),
latch_(1) // 计数器初值为1, 只需要等待一个线程任务完成
{
setDefaultName();
}
/**
* default Thread name: Thread + id (self-defined increased atomic id starts with 1)
*/
void Thread::setDefaultName()
{
int num = numCreated_.incrementAndGet();
if (name_.empty())
{
char buf[32];
snprintf(buf, sizeof(buf), "Thread%d", num);
name_ = buf;
}
}
1)latch_是用来解决调用线程和新线程的同步问题的。只有新线程准备好了以后,调用线程才能继续正常运行。因此,初值为1;
2)setDefaultName() 利用类的原子变量numCreated_,来组装构建线程对象的名称(name_)。
start()中创建线程,并启动线程函数;join()连接线程。
void Thread::start()
{
assert(!started_); //to avoid repeated start()
started_ = true;
// FIXME: move(func_)
detail::ThreadData* data = new detail::ThreadData(func_, name_, &tid_, &latch_);
if (pthread_create(&pthreadId_, NULL, &detail::startThread, data))
{ // non-zero: error
started_ = false;
delete data; // or no delete?
LOG_SYSFATAL << "Failed in pthread_create";
}
else
{ // zero: success
latch_.wait();
assert(tid_ > 0);
}
}
int Thread::join()
{
assert(started_);
assert(!joined_);
joined_ = true; // 置连接状态
return pthread_join(pthreadId_, NULL); // 连接线程
}
1)我们并没有直接启动线程函数,而是先构建一个自定义内部类ThreadData对象,包含了线程相关信息,然后再传递给新线程函数。
2)线程创建pthread_create失败时,调用LOG_SYSFATAL,会打印log并直接导致程序终止;成功时,会利用latch_等待新线程函数启动运行到指定位置(已经设置好线程tid)。
3)我们将pthread_create线程函数交给 detail::startThread来执行,而该函数内部又通过传入的ThreadData参数,将运行ThreadData::runInThread(),再在其中运行用户设置的线程函数。而这个函数,是在Thread构建时,由用户指定的。
内部类ThreadData
自定义的线程数据结构ThreadData,作为实现细节,包含在detail命名空间即可。
ThreadData主要实现:
1)新线程通用数据的封装;
2)新线程的启动与调用线程的同步;
3)try-catch 捕捉并处理用户传入的线程函数异常;
4)调用prctl修改线程在内核中的名称;
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)
{ }
/**
* set Thread name, tid
*
* run thread func set by ctor
*/
void runInThread()
{
*tid_ = muduo::CurrentThread::tid(); // help to cache current thread tid
tid_ = NULL;
latch_->countDown();
latch_ = NULL; // as latch_'s member count_ init value = 1, abandon it after countDown()
muduo::CurrentThread::t_threadName = name_.empty() ? "muduoThread" : name_.c_str();
// Set the name of the calling thread
::prctl(PR_SET_NAME, muduo::CurrentThread::t_threadName);
try {
func_();
muduo::CurrentThread::t_threadName = "finished";
}
catch (...)
{
...
}
}
};
当前线程CurrentThread
muduo有个特殊的命名空间muduo::CurrentThread,包含了线程的本地数据(thread local),以及对调用线程的若干操作。
thread local数据主要包括:
// CurrentThread.h
// thread local
extern __thread int t_cachedTid; // 缓存线程tid
extern __thread char t_tidString[32]; // 线程tid的字符串形式
extern __thread int t_tidStringLength; // t_tidStringLength的实际长度
extern __thread const char* t_threadName; // 线程名称
// CurrentThread.cc
__thread int t_cachedTid = 0;
__thread char t_tidString[32];
__thread int t_tidStringLength = 6;
__thread const char* t_threadName = "unknown";
static_assert(std::is_same<int, pid_t>::value, "pit_t should be int");
注意:这里有个static_assert,用于编译期断言线程tid的类型pid_t是否与int相同。
cacheTid()获取当前线程tid
前面Linux 获取线程id,已经提到:因为pthread_self()获得的 pthread_t类型的线程id,是glibc维护的一个动态分配的内存指针,而且是反复使用的,容易导致线程id值重复。因此我们用系统调用gettid,来获取Linux线程id。
考虑到线程id在线程创建后并不会改变,为了避免频繁系统调用,我们用thread local变量t_cachedTid在第一次请求线程id时,通过gettid系统调用缓存线程id,其他时候,直接返回该缓存值即可。
void CurrentThread::cacheTid()
{
if (t_cachedTid == 0)
{
t_cachedTid = detail::gettid();
t_tidStringLength = snprintf(t_tidString, sizeof(t_tidString), "%5d ", t_cachedTid);
}
}
pid_t detail::gettid()
{
return static_cast<pid_t>(::syscall(SYS_gettid));
}
isMainThread()判断调用线程是否为main线程
Linux中,线程本质上是通过进程来实现的,也就是说,新建线程对应tid跟pid的值是一样的。
/*
* Only main thread's tid == ::getpid()
*/
bool CurrentThread::isMainThread()
{
return tid() == ::getpid();
}
sleepUsec() 休眠指定微秒数
通过系统调用nanosleep实现休眠功能
void CurrentThread::sleepUsec(int64_t usec)
{
struct timespec ts = {0, 0};
ts.tv_sec = static_cast<time_t>(usec / Timestamp::kMicroSecondsPerSecond);
ts.tv_nsec = static_cast<long>(usec % Timestamp::kMicroSecondsPerSecond * 1000);
// std::this_thread::sleep_for(std::chrono::microseconds());
::nanosleep(&ts, NULL);
}
为什么不用usleep?
因为usleep在POSIX.1-2001不推荐使用, POSIX.1-2008 中已经废除。推荐使用nanosleep。当然,C++ 中还可以用std::this_thread::sleep_for。
ThreadNameInitializer类初始化main线程信息
有没有一种办法,能初始化main线程信息,包括线程名、tid?
答案是有的,可以设置一个全局对象,在构造时就初始化调用线程信息。
class ThreadNameInitializer
{
public:
ThreadNameInitializer() // 线程名称初始化
{
muduo::CurrentThread::t_threadName = "main"; // 初始化线程名
CurrentThread::tid(); // 缓存tid
pthread_atfork(NULL, NULL, &childAfterFork); // 清除fork子进程对应线程信息
}
};
static ThreadNameInitializer init; // 全局变量,会由main线程构造对象
由于线程信息在初始化以后,并不会自行改变:tid是缓存一次,线程名是不会变化。如果在main线程中,fork创建子进程,子进程对应线程也会继承父线程(main)的线程信息,显然,这不是我们想要的。我们需要专门为子进程清除从父进程继承而来的线程信息。
因此,需要通过pthread_atfork,在fork结束前,子进程中注册用于清理子进程的main线程信息的清理函数childAfterFork。
void childAfterFork()
{
muduo::CurrentThread::t_cachedTid = 0; // clear child tid
muduo::CurrentThread::t_threadName = "child";
CurrentThread::tid();
// no need to call pthread_atfork(NULL, NULL, &childAfterFork);
}
知识点
is_same模板判断两种类型是否相同
如果int和pid_t是同种类型,用is_same::value将返回true。
bool sameType = std::is_same<int, pid_t>::value;
muduo库其它部分解析参见:muduo库笔记汇总