muduo库-线程类Thread,当前线程类CurrentThread
线程类Thread要解决的问题
从用户角度,一个线程类应该要提供什么给用户?
线程类最核心的内容显然是为用户提供另一个执行流,让用户程序能以线程方式并发执行(调用线程与新线程“同时”执行),但同时能共享同一个进程的内存空间。同时,作为用户,我们希望能对这个线程设置用户提供的线程函数,还有对线程进行控制,包括启动、停止、回收资源(连接);获得这个线程在内核或线程库中的线程id,是否已启动、是否已连接(被回收资源)等状态信息。为了方便调试、打印/查看log,我们可能还需要为线程设置标识,如用户指定的线程id和线程名称等信息。
现有的线程能提供什么?
Linux下,C++ 11 std::thread
也是用NPTL提供的pthreads实现的,因此,我们主要考虑pthreads。
pthreads主要接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #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) ;int pthread_detach (pthread_t thread) ;void pthread_exit (void *retval) ;int pthread_cancel (pthread_t thread) ;int pthread_equal (pthread_t t1, pthread_t t2) ;
注:以上线程函数的使用,都需要用-pthread编译、链接。
封装线程类Thread
根据pthreads接口pthread_*,Thread要实现:
基本线程的原语:线程的创建和等待结束。
线程控制的状态:是否已经创建(启动),是否已经结束(连接)。
线程属性:线程id,线程名称。
线程统计信息:通过Thread class创建的线程数量。
线程类的拷贝没有实际意义,因为线程会对应内核中的数据结构,运行状态等。
Thread 接口
因此,我们可以为Thread设计如下接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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_; pid_t tid_; ThreadFunc func_; string name_; CountDownLatch latch_; static AtomicInt32 numCreated_; };
Thread 实现
Thread对象构造,决定了数据成员的初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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 ) { setDefaultName (); }void Thread::setDefaultName () { int num = numCreated_.incrementAndGet (); if (name_.empty ()) { char buf[32 ]; snprintf (buf, sizeof (buf), "Thread%d" , num); name_ = buf; } }
latch_是用来解决调用线程和新线程的同步问题的。只有新线程准备好了以后,调用线程才能继续正常运行。因此,初值为1;
setDefaultName()
利用类的原子变量numCreated_,来组装构建线程对象的名称(name_)。
start()中创建线程,并启动线程函数;join()连接线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void Thread::start () { assert (!started_); started_ = true ; detail::ThreadData* data = new detail::ThreadData (func_, name_, &tid_, &latch_); if (pthread_create (&pthreadId_, NULL , &detail::startThread, data)) { started_ = false ; delete data; LOG_SYSFATAL << "Failed in pthread_create" ; } else { 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修改线程在内核中的名称;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 (); latch_ = NULL ; muduo::CurrentThread::t_threadName = name_.empty () ? "muduoThread" : name_.c_str (); ::prctl (PR_SET_NAME, muduo::CurrentThread::t_threadName); try { func_ (); muduo::CurrentThread::t_threadName = "finished" ; } catch (...) { ... } } };
当前线程CurrentThread
muduo中有个很特殊的命名空间:muduo::CurrentThread。CurrentThread包含了线程的本地数据(thread
local),以及对调用线程的若干操作。
thread local数据主要包括:
1 2 3 4 5 6 7 8 9 10 11 12 13 extern __thread int t_cachedTid; extern __thread char t_tidString[32 ]; extern __thread int t_tidStringLength; extern __thread const char * t_threadName; __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
前面https://www.cnblogs.com/fortunely/p/15930558.html,已经提到:因为pthread_self()获得的
pthread_t类型的线程id,是glibc维护的一个动态分配的内存指针,而且是反复使用的,容易导致线程id值重复。因此我们用系统调用gettid,来获取Linux线程id。
考虑到线程id在线程创建后并不会改变,为了避免频繁系统调用,我们用thread
local变量t_cachedTid在第一次请求线程id时,通过gettid系统调用缓存线程id,其他时候,直接返回该缓存值即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 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的值是一样的。
1 2 3 4 5 6 7 bool CurrentThread::isMainThread () { return tid () == ::getpid (); }
sleepUsec() 休眠指定微秒数
通过系统调用nanosleep实现休眠功能
1 2 3 4 5 6 7 8 9 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 ); ::nanosleep (&ts, NULL ); }
为什么不用usleep?
因为usleep在POSIX.1-2001不推荐使用, POSIX.1-2008
中已经废除。推荐使用nanosleep。当然,C++
中还可以用std::this_thread::sleep_for。
ThreadNameInitializer类初始化main线程信息
有没有一种办法,能初始化main线程信息,包括线程名、tid?
答案是有的,可以设置一个全局对象,在构造时就初始化调用线程信息。
1 2 3 4 5 6 7 8 9 10 11 12 class ThreadNameInitializer {public : ThreadNameInitializer () { muduo::CurrentThread::t_threadName = "main" ; CurrentThread::tid (); pthread_atfork (NULL , NULL , &childAfterFork); } };static ThreadNameInitializer init;
由于线程信息在初始化以后,并不会自行改变:tid是缓存一次,线程名是不会变化。如果在main线程中,fork创建子进程,子进程对应线程也会继承父线程(main)的线程信息,显然,这不是我们想要的。我们需要专门为子进程清除从父进程继承而来的线程信息。
因此,需要通过pthread_atfork,在fork结束前,子进程中注册用于清理子进程的main线程信息的清理函数childAfterFork。
1 2 3 4 5 6 7 void childAfterFork () { muduo::CurrentThread::t_cachedTid = 0 ; muduo::CurrentThread::t_threadName = "child" ; CurrentThread::tid (); }
知识点
is_same模板判断两种类型是否相同
如果int和pid_t是同种类型,用is_same::value将返回true。
1 bool sameType = std::is_same<int , pid_t >::value;
本文转载自muduo笔记
线程类Thread,当前线程CurrentThread