添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

线程类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";
        { // 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库笔记汇总