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 事件
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/epoll.h> #include <pthread.h> #include <fcntl.h> #define MAX_EVENTS 1024 #define THREAD_POOL_SIZE 4 #define BUFFER_SIZE 4096 #define PORT 8080 // 任务队列结构 typedef struct { void (*task_function)(int); int client_fd; } Task; // 线程池结构 typedef struct { pthread_t *threads; Task *task_queue; int queue_size; int queue_capacity; int head; int tail; int count; pthread_mutex_t lock; pthread_cond_t task_cond; pthread_cond_t space_cond; } ThreadPool; // 全局变量 ThreadPool thread_pool; int epoll_fd; // 设置非阻塞套接字 void set_nonblocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); } // 初始化线程池 void thread_pool_init(ThreadPool *pool, int size, int capacity) { pool->threads = malloc(size * sizeof(pthread_t)); pool->task_queue = malloc(capacity * sizeof(Task)); pool->queue_size = 0; pool->queue_capacity = capacity; pool->head = 0; pool->tail = 0; pool->count = 0; pthread_mutex_init(&pool->lock, NULL); pthread_cond_init(&pool->task_cond, NULL); pthread_cond_init(&pool->space_cond, NULL); for (int i = 0; i < size; i++) { pthread_create(&pool->threads[i], NULL, (void *)worker_thread, pool); } } // 工作线程函数 void *worker_thread(void *arg) { ThreadPool *pool = (ThreadPool *)arg; while (1) { pthread_mutex_lock(&pool->lock); // 等待任务 while (pool->queue_size == 0) { pthread_cond_wait(&pool->task_cond, &pool->lock); } // 获取任务 Task task = pool->task_queue[pool->head]; pool->head = (pool->head + 1) % pool->queue_capacity; pool->queue_size--; pthread_cond_signal(&pool->space_cond); pthread_mutex_unlock(&pool->lock); // 执行任务 task.task_function(task.client_fd); // 处理完成后重新注册到epoll struct epoll_event event; event.events = EPOLLIN | EPOLLET; event.data.fd = task.client_fd; epoll_ctl(epoll_fd, EPOLL_CTL_MOD, task.client_fd, &event); } return NULL; } // 添加任务到线程池 void add_task(ThreadPool *pool, void (*task_function)(int), int client_fd) { pthread_mutex_lock(&pool->lock); // 等待队列空间 while (pool->queue_size == pool->queue_capacity) { pthread_cond_wait(&pool->space_cond, &pool->lock); } // 添加任务 pool->task_queue[pool->tail].task_function = task_function; pool->task_queue[pool->tail].client_fd = client_fd; pool->tail = (pool->tail + 1) % pool->queue_capacity; pool->queue_size++; pthread_cond_signal(&pool->task_cond); pthread_mutex_unlock(&pool->lock); } // 处理HTTP请求的函数 void handle_request(int client_fd) { char buffer[BUFFER_SIZE]; ssize_t bytes_read = recv(client_fd, buffer, BUFFER_SIZE - 1, 0); if (bytes_read > 0) { buffer[bytes_read] = '\0'; // 简单的HTTP响应 const char *response = "HTTP/1.1 200 OK\r\n" "Content-Type: text/plain\r\n" "Content-Length: 31\r\n\r\n" "Hello from C epoll + thread pool!"; send(client_fd, response, strlen(response), 0); } // 注意:实际应用中需要处理Keep-Alive close(client_fd); } int main() { // 创建服务器套接字 int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { perror("socket"); exit(EXIT_FAILURE); } // 设置SO_REUSEADDR int opt = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 绑定地址 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { perror("bind"); exit(EXIT_FAILURE); } // 监听 if (listen(server_fd, SOMAXCONN) == -1) { perror("listen"); exit(EXIT_FAILURE); } // 创建epoll实例 epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); } // 添加服务器套接字到epoll struct epoll_event event; event.events = EPOLLIN | EPOLLET; event.data.fd = server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) { perror("epoll_ctl: server_fd"); exit(EXIT_FAILURE); } // 初始化线程池 thread_pool_init(&thread_pool, THREAD_POOL_SIZE, 256); // 事件循环 struct epoll_event events[MAX_EVENTS]; printf("Server running on port %d...\n", PORT); while (1) { int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait"); exit(EXIT_FAILURE); } for (int i = 0; i < nfds; i++) { // 新连接 if (events[i].data.fd == server_fd) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd == -1) { perror("accept"); continue; } // 设置非阻塞 set_nonblocking(client_fd); // 添加到epoll event.events = EPOLLIN | EPOLLET; event.data.fd = client_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) { perror("epoll_ctl: client_fd"); close(client_fd); } } // 客户端数据 else if (events[i].events & EPOLLIN) { int client_fd = events[i].data.fd; // 从epoll中暂时移除 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL); // 添加到线程池 add_task(&thread_pool, handle_request, client_fd); } } } close(server_fd); return 0; } 网页加载非常缓慢
最新发布
08-05
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值