muduo库学习篇-Thread类学习
多线程编程在任何语言中基本都是一个绕不开的话题,如果我们想要发挥计算机多核的优势,提高程序的响应速度,就一定要使用到多线程编程技术。因此muduo库一定少不了thread的封装,接下来我们开始学习muduo库thread类的封装。
如果让我自己设计一个thread类的话我能想到的有哪些:
- 成员变量:线程ID、线程回调函数指针
- 成员函数:线程的创建(构造函数)、线程的回收函数、线程分离函数
上面是我能想到的自己设计一个线程类时应该有的属性,我们再看看muduo库的线程比我自己想到多了哪些
Thread.h
// Use of this source code is governed by a BSD-style license
// that can be found in the License file.
//
// Author: Shuo Chen (chenshuo at chenshuo dot com)
#ifndef MUDUO_BASE_THREAD_H
#define MUDUO_BASE_THREAD_H
#include "muduo/base/Atomic.h"
#include "muduo/base/CountDownLatch.h"
#include "muduo/base/Types.h"
#include <functional>
#include <memory>
#include <pthread.h>
namespace muduo
{
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();
bool started_;
bool joined_;
pthread_t pthreadId_;
pid_t tid_;
ThreadFunc func_;
string name_;
CountDownLatch latch_;
static AtomicInt32 numCreated_;
};
} // namespace muduo
#endif // MUDUO_BASE_THREAD_H
muduo库thread类的设计:
- muduo库继承自noncopyable类,说明了线程是不可以进行拷贝的
- 多了两个bool变量,started 和 joined 通过这两个变量可以让线程的调用者知道线程是否已经启动或者回收
- pid_t 进程id 在linux下可以通过系统调用获取线程唯一标识的ID,在linux下线程是一个轻量级进程,也有自己的进程ID,可以通过系统调用::syscall(SYS_gettid)获取。
- 回调的函数指针通过typedef重命名
- 线程名称
- static AtomicInt32原子整型变量,用来记录当前进程中线程的个数
- CountDownLatch这个类型先不管,后面再说
上面这是通过对muduo线程类头文件我在第一时间没有能想到的点,上面这些点虽然有些我没能想到,但是也和需求有一定的关系,但是其中pid_t这个点的话是属于知识盲区,以前是不知道可以通过系统调用获取线程的进程id(每个线程唯一)。
接下来我们学习muduo库thread.cc,看看线程的实现
// Use of this source code is governed by a BSD-style license
// that can be found in the License file.
//
// Author: Shuo Chen (chenshuo at chenshuo dot com)
#include "muduo/base/Thread.h"
#include "muduo/base/CurrentThread.h"
#include "muduo/base/Exception.h"
#include "muduo/base/Logging.h"
#include <type_traits>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <linux/unistd.h>
namespace muduo
{
namespace detail
{
pid_t gettid()
{
return static_cast<pid_t>(::syscall(SYS_gettid));
}
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;
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 (const Exception& ex)
{
muduo::CurrentThread::t_threadName = "crashed";
fprintf(stderr, "exception caught in Thread %s\n", name_.c_str());
fprintf(stderr, "reason: %s\n", ex.what());
fprintf(stderr, "stack trace: %s\n", ex.stackTrace());
abort();
}
catch (const std::exception& ex)
{
muduo::CurrentThread::t_threadName = "crashed";
fprintf(stderr, "exception caught in Thread %s\n", name_.c_str());
fprintf(stderr, "reason: %s\n", ex.what());
abort();
}
catch (...)
{
muduo::CurrentThread::t_threadName = "crashed";
fprintf(stderr, "unknown exception caught in Thread %s\n", name_.c_str());
throw; // rethrow
}
}
};
void* startThread(void* obj)
{
ThreadData* data = static_cast<ThreadData*>(obj);
data->runInThread();
delete data;
return NULL;
}
} // namespace detail
void CurrentThread::cacheTid()
{
if (t_cachedTid == 0)
{
t_cachedTid = detail::gettid();
t_tidStringLength = snprintf(t_tidString, sizeof t_tidString, "%5d ", t_cachedTid);
}
}
bool CurrentThread::isMainThread()
{
return tid() == ::getpid();
}
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);
}
AtomicInt32 Thread::numCreated_;
Thread::Thread(ThreadFunc func, const string& n)
: started_(false),
joined_(false),
pthreadId_(0),
tid_(0),
func_(std::move(func)),
name_(n),
latch_(1)
{
setDefaultName();
}
Thread::~Thread()
{
if (started_ && !joined_)
{
pthread_detach(pthreadId_);
}
}
void Thread::setDefaultName()
{
int num = numCreated_.incrementAndGet();
if (name_.empty())
{
char buf[32];
snprintf(buf, sizeof buf, "Thread%d", num);
name_ = buf;
}
}
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();
assert(tid_ > 0);
}
}
int Thread::join()
{
assert(started_);
assert(!joined_);
joined_ = true;
return pthread_join(pthreadId_, NULL);
}
} // namespace muduo
查看muduo库thread.cc我们发现代码分为两部分,detail命名空间部分是实现是用的一些全局函数和数据结构,Thread::部分是线程成员函数的实现,我们的学习也分为两部分。
detail部分:
-
gettid()
- 函数返回一个pid_t类型变量,获取线程的进程ID,static_cast<pid_t>(::syscall(SYS_gettid))通过系统调用获取返回,
- static_cast 是c++的类型转换,之前一直习惯使用C风格的()转换,以后要习惯使用
-
afterFork()
-
afterFork函数用到了currentThread命名空间,进入currentThread命名空间,代码如下:
_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, "pid_t should be int");
可以看到有几个全局变量和一个断言函数,发现没个全局变量前面都有__thread 修饰,那么 __thread有什么用
-
__thread
__thread是GCC内置的线程局部存储设施,存取效率可以和全局变量相比。__thread变量每一个线程有一份独立实体,各个线程的值互不干扰。可以用来修饰那些带有全局性且值可能变,但是又不值得用全局变量保护的变量。
__thread能修饰POD全局类型(类似整型指针的标量,不带自定义的构造、拷贝、赋值、析构的类型,二进制内容可以任意复制memset,memcpy,且内容可以复原),不能修饰class类型,因为无法自动调用构造函数和析构函数,可以用于修饰全局变量,函数内的静态变量,不能修饰函数的局部变量或者class的普通成员变量
通过使用__thread类型的变量,上面的数据在每个线程中就都有一份而且没有相互覆盖的情况,这个地方如果不知道这种做法的只能通过一个全局的数组来记录了,操作的时候还需上锁,既影响效率还麻烦容易出错
-
static_assert在编译期间就可以进行断言,详情见前面博客
-
-
ThreadNameInitializer初始化类
class ThreadNameInitializer { public: ThreadNameInitializer() { muduo::CurrentThread::t_threadName = "main"; CurrentThread::tid(); pthread_atfork(NULL, NULL, &afterFork); } }; ThreadNameInitializer init;
-
pthread_atfork()
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
q调用fork时,内部创建子进程前在父进程中会调用prepare,内部创建子进程成功后,父进程会调用parent ,子进程会调用child
这个类是用来给调用fork后的子进程中的主线程创建名称的,在多线程中如果其中一个线程调用fork函数后,子进程中不会有其他线程,只有调用fork线程的堆栈,所以我们可以看到这个地方muduo线程名称默认设置为main,因为对于子进程而言,就是main线程,通过pthread_atfork实现。
拓展
如果我们多线程中有对锁的使用,同时又调用了fork那么就可能会出现死锁的现象,那么我们这时候就可以通过调用pthread_atfork()函数来避免死锁。
看代码:
#include <stdio.h> #include <time.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void* doit(void* arg) { printf("pid = %d begin doit ...\n",static_cast<int>(getpid())); pthread_mutex_lock(&mutex); struct timespec ts = {2, 0}; nanosleep(&ts, NULL); pthread_mutex_unlock(&mutex); printf("pid = %d end doit ...\n",static_cast<int>(getpid())); return NULL; } void prepare(void) { pthread_mutex_unlock(&mutex); } void parent(void) { pthread_mutex_lock(&mutex); } int main(void) { pthread_atfork(prepare, parent, NULL); printf("pid = %d Entering main ...\n", static_cast<int>(getpid())); pthread_t tid; pthread_create(&tid, NULL, doit, NULL); struct timespec ts = {1, 0}; nanosleep(&ts, NULL); if (fork() == 0) { doit(NULL); } pthread_join(tid, NULL); printf("pid = %d Exiting main ...\n",static_cast<int>(getpid())); return 0; }
上面代码,我们在主进程中创建了一个线程,子线程回调函数中调用mtx.lock()后睡眠两分钟,主进程主线程在创建子线程后睡眠了一分钟之后调用fork,子进程调用doit函数,如果fork先于子线程的mtx.unlock(),那么子进程中mtx就会处于lock,这时子进程再次加锁,就会死锁。但是我们通过pthread_atfork(prepare, parent, NULL);函数注册了prepare函数和parent函数,主进程在调用fork之前会先调用票prepare进行解锁,fork之后的子进程的mtx是没有加锁的,fork之后主进程调用parent函数,再次上锁,这样就避免了死锁的,问题。
-
-
cacheTid函数,通过该函数,每个线程的那几个全局变量就不用每次都去获取,因为获取线程的进程ID是系统调用,相对来说比较费时,这样可以提高效率
-
isMainThread通过线程的pid和进程的pid做比较如果相同说明是主线程
线程成员函数的实现部分
- ~Thread()析构函数,可以看到调用了pthread_detach函数,通过调用pthread_detach函数,子线程会和主线程分离,主线程不需要通过pthread_join函数进行回收,在这个地方muduo的作者把线程的分离放在了析构函数内,不是很能够理解
- start函数内封装了线程的创建函数pthread_create,线程的构造函数只是进行了简单的变量赋值,没想明白为什么要把线程的创建放在单独的start函数中实现,不知道muduo的作者处于目的,参考c++11中thread库发现,c++11中线程是在构造的时候就创建了,而不是通过调用start函数来实现,个人还是推崇c++11的做法
疑惑:
可以看到在thread的实现部分,作者将设计为两部分,一开始不是很能搞明白这样做的目的,可能和几个__thread变量有关系,_____thread变量是全局变量,作者为了对全局变量的使用而这样设计,这只是我的猜想,真正原因是什么不知道,确实看不出这样设计的好处。欢迎指导的小伙伴留言
上一篇: 努比亚红魔游戏手机即将发布:外观吸睛
下一篇: 中兴红牛V5 电信日三重惊喜