52. 線程-IO複用(EPOLLONESHOT)


線程怎麼與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] 又有什麼優勢呢?

  1. 只爲就緒的TCP連接創建線程, 而不必爲每個TCP連接創建一個線程; 這樣就可以減少內存的消耗.
  2. 線程的性能與IO複用的性能基本一樣, 所以可以使用線程處理連接來代替IO複用.

可能頻繁創建線程會導致性能下降, 但是我們也可以採用線程池[4]來解決這個問題, 所以這裏的線程函數可以稱爲工作線程.


小結

  • 清楚 EPOLLONESHOT 事件
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章