一起來寫web server 08 -- 多執行緒+非阻塞IO+epoll
一起來寫web server 08 -- 多執行緒+非阻塞IO+epoll
到了多執行緒,一些東西就變得耐人尋味了.
這個版本是在前面單執行緒epoll
的基礎上引入了執行緒池,當然不是前面玩具一樣的執行緒池,而是一個通用的元件,生產者消費者佇列.
生產者消費者佇列
生產者消費者問題是作業系統中一個很經典的同步互斥問題,已經有了很不錯的解決方案,將它的解決方案擴充一下,就可以用於我們的實踐啦.
我自己寫了一個生產者消費者的佇列,然後發現muduo
中已經內建了這種模型,而且使用起來比我寫的更加順手,所以我就引用它的實現,我這裡稍微來講解一下它的實現,然後我會順帶講解一下我的思路.
muduo
庫的生產者消費者模型
這是ThreadPool
類的一個宣告:
class ThreadPool : noncopyable
{
public:
typedef boost::function<void()> Task; /* 需要執行的任務 */
private:
bool isFull();
Task take();
size_t queueSize();
int threadNum_; /* 執行緒的數目 */
int maxQueueSize_;
std::list<Task> queue_; /* 工作佇列 */
MutexLock mutex_;
Condition notEmpty_;
Condition notFull_;
};
這裡的boost::function
其實在cpp 11
標準中已經加入了,你如果沒有安裝boost庫的話,可以快取std
的版本,效果是一樣的.因為boost
本來就是cpp
的std
的一個備選庫.
為什麼要使用boost::function
不用我多說,你可以檢視這裡:http://blog.csdn.net/solstice/article/details/3066268
我們來看一下程式碼的實現,首先是建構函式:
ThreadPool::ThreadPool(int threadNum, int maxQueueSize)
: threadNum_(threadNum)
, maxQueueSize_(maxQueueSize)
, mutex_()
, notEmpty_(mutex_)
, notFull_(mutex_)
{
assert(threadNum >= 1 && maxQueueSize >= 1);
/* 接下來要構建threadNum個執行緒 */
pthread_t tid_t;
for (int i = 0; i < threadNum; i++) {
Pthread_create(&tid_t, NULL, startThread, this);
}
}
這裡ThreadPool
有兩個條件變數,一個是notEmpty_
,一個是notFull_
,建構函式接受兩個引數,一個是執行緒的數目,一個是最大的佇列的大小.
接下來是所有的執行緒都執行的函式startThread
:
void* ThreadPool::startThread(void* obj)
{ /* 工作者執行緒 */
Pthread_detach(Pthread_self());
ThreadPool* pool = static_cast<ThreadPool*>(obj);
pool->run();
return pool;
}
它們都開始呼叫run
函式:
void ThreadPool::run()
{
for ( ; ; ) { /* 一直執行下去 */
Task task(take());
if (task) {
//mylog("task run!");
task();
}
//mylog("task over!");
}
}
run
函式非常簡單,就是不斷從佇列中取出任務,然後執行任務,沒有任務的話,會阻塞在那裡.
我們來看take
函式:
ThreadPool::Task ThreadPool::take()
{
MutexLockGuard lock(mutex_); /* 加鎖 */
while (queue_.empty()) { /* 如果佇列為空 */
notEmpty_.wait(); /* 等待 */
}
Task task;
if (!queue_.empty()) {
task = queue_.front();
queue_.pop_front();
if (maxQueueSize_ > 0) { /* 通知生產者佇列有空位置了 */
notFull_.notify();
}
}
//mylog("threadpool take 1 task!");
return task;
}
對於生產者而言,有一個非常重要的函式,那就是append
:
bool ThreadPool::append(Task&& task)
{ /* 使用了右值引用 */
{
MutexLockGuard lock(mutex_); /* 首先加鎖 */
while (isFull()) { /* 如果佇列已滿 */
notFull_.wait(); /* 等待queue有空閒位置 */
}
assert(!isFull());
queue_.push_back(std::move(task)); /* 直接用move語義,提高了效率 */
//mylog("put task onto queue!");
}
notEmpty_.notify(); /* 通知消費者有任務可做了 */
}
生產者消費者佇列的程式碼就是這麼簡單,但是muduo
庫寫的確實很漂亮.
我的思路
其實程式碼基本上和前面的類似,不同的是,我壓根就沒有考慮過使用boost::funciton
和boost::bind
這對神器,因為我之前也壓根就沒有這樣編過碼.
如果不用boost::funciton
和boost::bind
這兩樣東西,我們要實現類似的程式碼的話,可能的一個解決方案是使用模版(template
).
佇列裡面放的是T
型別,然後消費者取出一個T
型別,呼叫T
型別的一個run
或者別的什麼不帶引數的方法.這樣以來,對T
型別就有了限制,要求T
型別必須實現run
之類的方法.
而且程式碼變得不太容易讀.加了模版的玩意總是不容易讀,不是嗎?所以要積極使用cpp
的新特性.
主程式變成了生產者
這一次的程式碼變得簡潔多了,
int main(int argc, char *argv[])
{
int listenfd = Open_listenfd(8080); /* 8080號埠監聽 */
epoll_event events[MAXEVENTNUM];
sockaddr clnaddr;
socklen_t clnlen = sizeof(clnaddr);
block_sigpipe(); /* 首先要將SIGPIPE訊息阻塞掉 */
int epollfd = Epoll_create(1024); /* 10基本上沒有什麼用處 */
addfd(epollfd, listenfd, false); /* epollfd要監聽listenfd上的可讀事件 */
ThreadPool pools(10, 30000); /* 10個執行緒,300個任務 */
HttpHandle::setEpollfd(epollfd);
HttpHandle handle[2000];
for ( ; ;) {
int eventnum = Epoll_wait(epollfd, events, MAXEVENTNUM, -1);
for (int i = 0; i < eventnum; ++i) {
int sockfd = events[i].data.fd;
if (sockfd == listenfd) { /* 有連線到來 */
//mylog("connection comes!");
for ( ; ; ) {
int connfd = accept(listenfd, &clnaddr, &clnlen);
if (connfd == -1) {
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { /* 將連線已經建立完了 */
break;
}
unix_error("accept error");
}
handle[connfd].init(connfd); /* 初始化 */
addfd(epollfd, connfd, false); /* 加入監聽 */
}
}
else { /* 有資料可讀或者可寫 */
pools.append(boost::bind(&HttpHandle::process, &handle[sockfd]));
}
}
}
return 0;
}
注意最後的一句boost::bind(&HttpHandle::process, &handle[sockfd])
,直接將物件往函式上一繫結,就往佇列裡面扔.非常爽.
這一次,我們終於將SIGPIPE訊息給忽略掉了,主要是呼叫下面這個函式:
void block_sigpipe()
{
sigset_t signal_mask;
sigemptyset(&signal_mask);
sigaddset(&signal_mask, SIGPIPE);
int rc = pthread_sigmask(SIG_BLOCK, &signal_mask, NULL);
if (rc != 0) {
printf("block sigpipe error\n");
}
}
shared_ptr並不是執行緒安全的
正如文章開頭所講的,多執行緒一來,很多事情就變得莫名奇妙了,比如說shared_ptr
,因為這個玩意的執行緒不安全性,我調了半天bug
,才發現原來是Cache的查詢函式出了問題,下面是修改過後的執行緒安全版本的函式:
/* 執行緒安全版本的getFileAddr */
void getFileAddr(std::string fileName, int fileSize, boost::shared_ptr<FileInfo>& ptr) {
/*-
* shared_ptr並不是執行緒安全的,對其的讀寫都需要加鎖.
*/
MutexLockGuard lock(mutex_);
if (cache_.end() != cache_.find(fileName)) { /* 如果在cache中找到了 */
ptr = cache_[fileName];
return;
}
if (cache_.size() >= MAX_CACHE_SIZE) { /* 檔案數目過多,需要刪除一個元素 */
cache_.erase(cache_.begin()); /* 直接移除掉最前一個元素 */
}
boost::shared_ptr<FileInfo> fileInfo(new FileInfo(fileName, fileSize));
cache_[fileName] = fileInfo;
ptr = std::move(fileInfo); /* 直接使用move語義 */
}
至於為什麼不安全,可以檢視這裡,寫的再好不過了:http://blog.csdn.net/solstice/article/details/8547547
多執行緒的除錯
原諒我到了這接近尾聲的時候,才提起多執行緒的除錯,首先要說一句的是,多執行緒真的不太好調,因為很難重現錯誤,但是我在這裡稍稍介紹一下我的技巧.
列印
列印算是屢試不爽的一種方法,對於我們這個簡陋的web server
,我封裝了一個日誌函式mylog
:
void mlog(pthread_t tid, const char *fileName, int lineNum, const char *func, const char *log_str, ...)
{
va_list vArgList; //定義一個va_list型的變數,這個變數是指向引數的指標.
char buf[1024];
va_start(vArgList, log_str); //用va_start巨集初始化變數,這個巨集的第二個引數是第一個可變引數的前一個引數,是一個固定的引數
vsnprintf(buf, 1024, log_str, vArgList); //注意,不要漏掉前面的_
va_end(vArgList); //用va_end巨集結束可變引數的獲取
printf("%lu:%s:%d:%s --> %s\n", tid, fileName, lineNum, func, buf);
}
然後定義了一個巨集,方便使用這個函式:
#define mylog(formatPM, args...)\
mlog(pthread_self(), __FILE__, __LINE__, __FUNCTION__, (formatPM) , ##args)
需要日誌的時候,可以像printf
函式一樣使用:
mylog("My simple web server! %d, %s\n", 1, "hello, workd!");
這個巨集展開後會呼叫mlog
函式,列印出行,檔名,函式名等資訊,對付我們這個小玩意足夠了.
用VS來除錯
VS
其實也內建了執行緒的除錯,你可以結合Visual Gdb
一起來除錯linux
下的程式碼.一兩個執行緒問題倒是不大,不過執行緒多了的話,這個玩意就不好調了,要我說,最好的方法還是分析日誌.
總結
這個版本已經算是比較強勁的一個版本了,修復了前面的一些bug
,但是引入了新的bug
,這個bug
我也是折騰了很久才弄出來.
一般在單執行緒下不可能出現這樣的bug
,只有在多執行緒的條件下,這樣的程式碼才變成了bug
,正如前面見到的,每個HttpHandle
處理一個連線,試想這樣一種情形:客戶端不知道因為什麼原因,第一次傳送了這樣的資料:
GET /
隔了很短時間才會傳送餘下的資料.這時,第一次傳送的資料正在被另外一個執行緒處理,在多執行緒條件下,對於第二次到來的資料,這個HttpHandle
會交由另外一個執行緒處理,也就是說,有兩個執行緒在不加鎖地使用同一個HttpHandle
,不出問題才怪.
解決方案是有的,那就是EPOLL
的ONESHOT
引數.不過那是下一個版本的故事啦.
和之前類似的,程式碼在這裡:https://github.com/lishuhuakai/Spweb
相關文章
- 非同步/同步,阻塞/非阻塞,單執行緒/多執行緒概念梳理非同步執行緒
- 伺服器模型——從單執行緒阻塞到多執行緒非阻塞(中)伺服器模型執行緒
- 伺服器模型——從單執行緒阻塞到多執行緒非阻塞(下)伺服器模型執行緒
- 伺服器模型——從單執行緒阻塞到多執行緒非阻塞(上)伺服器模型執行緒
- 那些年搞不懂的多執行緒、同步非同步及阻塞和非阻塞(一)---多執行緒簡介執行緒非同步
- 單執行緒-非阻塞-長連結執行緒
- 程式執行緒、同步非同步、阻塞非阻塞、併發並行執行緒非同步並行
- java多執行緒:執行緒池原理、阻塞佇列Java執行緒佇列
- 聊聊執行緒與程式 & 阻塞與非阻塞 & 同步與非同步執行緒非同步
- 併發-0-同步/非同步/阻塞/非阻塞/程式/執行緒非同步執行緒
- 對執行緒、協程和同步非同步、阻塞非阻塞的理解執行緒非同步
- 那些年搞不懂的多執行緒、同步非同步及阻塞和非阻塞(二)---概念區分執行緒非同步
- 程式與執行緒、同步與非同步、阻塞與非阻塞、併發與並行執行緒非同步並行
- 執行緒的阻塞執行緒
- Java多執行緒/併發08、中斷執行緒 interrupt()Java執行緒
- Java BlockingQueue 阻塞佇列[用於多執行緒]JavaBloC佇列執行緒
- 深入淺出Java多執行緒(十三):阻塞佇列Java執行緒佇列
- suging閒談-netty 的非同步非阻塞IO執行緒與業務執行緒分離Netty非同步執行緒
- GCD 多執行緒安全 單寫多讀GC執行緒
- 非同步與執行緒阻塞非同步執行緒
- 來吧!再談多執行緒執行緒
- 多執行緒和多執行緒同步執行緒
- java多執行緒8:阻塞佇列與Fork/Join框架Java執行緒佇列框架
- 多執行緒【執行緒池】執行緒
- 多執行緒--執行緒管理執行緒
- Java多執行緒——執行緒Java執行緒
- 執行緒與多執行緒執行緒
- VC多執行緒 C++ 多執行緒執行緒C++
- 多執行緒,執行緒類三種方式,執行緒排程,執行緒同步,死鎖,執行緒間的通訊,阻塞佇列,wait和sleep區別?執行緒佇列AI
- 多執行緒-執行緒控制之休眠執行緒執行緒
- 多執行緒-執行緒控制之加入執行緒執行緒
- 多執行緒-執行緒控制之禮讓執行緒執行緒
- 多執行緒-執行緒控制之中斷執行緒執行緒
- 非同步阻塞,Manager模組,執行緒非同步執行緒
- 執行緒的讓步與阻塞執行緒
- mysql 5.7 執行緒阻塞處理MySql執行緒
- 最全java多執行緒總結3——瞭解阻塞佇列和執行緒安全集合不Java執行緒佇列
- 使用Actor模型管理Web Worker多執行緒模型Web執行緒