多線程編程在任何語言中基本都是一個繞不開的話題,如果我們想要發揮計算機多核的優勢,提高程序的響應速度,就一定要使用到多線程編程技術。因此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的做法