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库笔记汇总

posted @ 2022-02-28 21:30  明明1109  阅读(689)  评论(5编辑  收藏  举报