線程怎麼與IO複用聯繫起來, IO複用中創建線程? 還是線程中IO複用? 這個問題用在進程也是一樣的. 其實兩種方式都可以. 本節採用在 IO複用中創建線程, 接下來就來看看具體怎麼實現的吧.
epoll 的EPOLLONESHOT事件
還記得 epoll
的 event 可設置的狀態嗎? 忘了也不急, 這裏將 IO複用之epoll函數 的狀態粘貼過來.
event值 | 描述 |
---|---|
EPOLLIN | 監聽是描述符是否可讀 |
EPOLLOUT | 監聽是描述符是否可寫 |
EPOLLERR | 發生錯誤 |
EPOLLHUP | 對端掛斷, 或其中一端關閉了 |
EPOLLET[1] | 設置爲邊沿觸發模式 |
EPOLLONESHOT | 設置關聯文件描述符的一次性行爲 |
在epoll中直接創建函數 (如下面僞代碼), 如果緩衝區的數據沒有一次性讀完 (這種情況肯定會出現) 特別是LE模式[[2]]下又會立即觸發讀事件然後再次創建新的線程. 這樣就會導致同一時間段有兩個線程在處理同一個TCP連接.
if(event[i].event & EPOLLIN){
pthreaed_create(fun);
}
具體的解決辦法就是將文件描述符註冊 EPOLLONESHOT
事件就能保證該描述符只能被觸發一次, 如果描述符還需要需要被觸發, 則在處理後重新註冊. 如下 :
fun(int eventfd, int fd){
/* 處理過程 */
...
// 重新註冊
epoll_event event;
event.fd = fd;
event.event = EPOLLIN | EPOLLONESHOT;
epoll_ctl(eventfd, EPOLL_CTL_ADD, fd, &event);
}
實驗
依舊是線程完成回射部分, 與上節不一樣在於 : 文件描述符是非阻塞的, 我們通過sleep(1)
模仿處理過程, 如果處理完後沒有數據了或者連接斷開, 則線程就直接退出; 如果是因爲超時, 則將文件描述符重新註冊到監聽事件中.
部分代碼如下 :
// 回射線程
void *workecho(void *arg){
char buf[BUFSIZE];
int n;
int sockfd, epollfd;
struct eventfds *fds;
fds = (struct eventfds *)arg;
sockfd = fds->sockfd;
epollfd = fds->epollfd;
while(1){
n = recv(sockfd, buf, sizeof(buf), MSG_WAITALL);
if(n == 0){
close(sockfd);
printf("client close\n");
epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, NULL);
break;
}
else if(n < 0){
// 如果一秒內沒有數據, 則重新註冊事件並退出線程
if(errno == EAGAIN){
fprintf(stderr, "read timeout\n");
reset_event(epollfd, sockfd);
break;
}
}
else{
write(sockfd, buf, n);
sleep(1); // 睡眠一秒, 代表數據處理過程
}
}
printf("exit\n");
pthread_exit((void *)0);
}
主函數採用 epoll
監聽文件描述符, 並將連接的描述符置爲非阻塞, 狀態設置爲EVENTONESHOT, 保證一個文件描述只能有一個線程進行處理.
部分代碼 :
int main(int argc, char *argv[]){
...
listen(servfd, 5);
epollfd = epoll_create(1);
setevent(epollfd, servfd, 0); // servfd 監聽描述符不能設置爲一次性執行
int n;
while(1){
n = epoll_wait(epollfd, evs, sizeof(evs), -1);
for(int i = 0; i < n; ++i){
if(evs[i].data.fd == servfd){
clifd = accept(servfd, (struct sockaddr *)&cliaddr, &len);
if(clifd < 0)
goto exit;
setevent(epollfd, clifd, 1);
}
else if(evs[i].events & EPOLLIN){
fds.sockfd = clifd;
fds.epollfd = epollfd;
// 傳入注意 fds 並非是線程安全的
pthread_create(&tid, NULL, workecho, (void *)&fds);
}
}
}
close(servfd);
exit:
exit(0);
}
服務端 : service.c
gcc service.c -o service -pthread
./service 8080
客戶端 : client.c
./client 127.0.0.1 8080
可以看出來一段時間沒有數據則線程就會退出. 那麼修改程序與前面的 線程回射[3] 又有什麼優勢呢?
- 只爲就緒的TCP連接創建線程, 而不必爲每個TCP連接創建一個線程; 這樣就可以減少內存的消耗.
- 線程的性能與IO複用的性能基本一樣, 所以可以使用線程處理連接來代替IO複用.
可能頻繁創建線程會導致性能下降, 但是我們也可以採用線程池[4]來解決這個問題, 所以這裏的線程函數可以稱爲工作線程.
小結
- 清楚 EPOLLONESHOT 事件