云计算百科
云计算领域专业知识百科平台

多线程服务器分析——Reactor线程模型和性能分析(序章)

1 引言

最近在拜读陈硕的《Linux多线程服务端编程》[1],目前读到第3章,主要讲常用的服务端的编程模型,其中提到了很多线程模型以及与性能相关的分析,读了之后想实践一下书中提到的理论,也顺便复习一下网络编程的相关内容,于是搜索了很多关于Reactor和Proactor相关的内容,对于里面概念的讲述是可以理解的,但是如果具象化到代码中还是无法第一时间在脑中形成概念。因此想结合书中第三章的讲解,捋一捋多线程服务器的模型以及性能分析,通过C++代码逐步弄明白各种服务端模型和IO模型。没看过《Linux多线程服务端编程》的读者不必担心,本文只是以书为线索尽量全面的讲述多线程服务器的设计,涉及到书中的内容时会引用相关的原文。

2 阅读导引

  • 阅读本文首先要了解不同的IO模型,推荐阅读理解一下5种IO模型、阻塞IO和非阻塞IO、同步IO和异步IO [2];
  • 其次要了解socket编程,要了解socket的系统调用的功能;
  • 本文中的代码仅仅是为了方便展现多线程而实现,因此会缺乏关于异常处理或者日志系统等模块的代码;
  • 所有的代码均可在C++实现的多线程分布式存储服务器代码获取,每一章节均有对应。

索引:多线程服务器分析——Reactor线程模型和性能分析(索引)

下面我们正式开始:

3 阻塞服务器

3.1 单线程阻塞服务器

【代码reactor/single_thread】

你是一个卖煎饼果子的小老板,每天推着小车到公司门口,每到中午你的小车前总能排起一个不长不短的队伍。来一个顾客就摊一个煎饼果子,并且你只能摊完当前这个顾客并且交给他才能给下一个顾客摊,就像个"无情的摊煎饼果子服务器",这就是最基础的单线程阻塞服务器。

说煎饼果子只是给读者类比一下,下面用具体的代码进行讲解: 首先来创建客户端和服务端的代码,首先服务端就利用linux提供的socket相关的系统调用做一个tcp服务器,整体服务端要做的是接收客户端字符串并返回给客户端"hello world",其中handle_request()函数是处理客户端字符串的过程,当前是打印到屏幕,为了更直观的展示不同类型服务器模型的处理耗时,我们给服务端的处理加一个sleep 1毫秒,这样做的原因是线程的创建也是有开销的,如果服务端的处理只是打印那么创建线程的时间对于总体上的耗时会有很大的影响,我们可能无法清晰的感知到多线程和单线程的区别。

#define SERVER_SLEEP_MILLISEC 1

void handle_request(int connfd) {
char buffer[512];
memset(buffer, 0, 512);
int res = read(connfd, buffer, sizeof(buffer));

std::this_thread::sleep_for(std::chrono::milliseconds(SERVER_SLEEP_MILLISEC));
std::cout << "Server Received: " << buffer << std::endl;

char response[] = "hello world";
res = write(connfd, response, sizeof(response));

shutdown(connfd, SHUT_RD);
close(connfd);
}

int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(8080);

bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(sockfd, 1100);

while (1)
{
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int connfd = accept(sockfd, (struct sockaddr*)&cli_addr, &cli_len);
handle_request(connfd);
}
}

接着我们来构建客户端client,首先对client编号并将"i am client"和编号组合成字符串发送给服务端:

#define MAX_CLIENT 1000

void request_server(int i) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &(serv_addr.sin_addr));

int res = connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

char request[128] = "i am client ";
char num[128];
sprintf(num, "%d", i);
strcat(request, num);
res = write(sockfd, request, sizeof(request));

char response[512];
memset(response, 0, 512);
res = read(sockfd, response, sizeof(response));
std::cout << "Client Received: " << response << std::endl;

shutdown(sockfd, SHUT_RD);
close(sockfd);
}

int main()
{
/* for loop request server */
for(int i = 0; i < MAX_CLIENT; ++i) {
request_server(i);
}

/* multi-threaded request server */
std::vector<std::thread> threads;
for(int i = 0; i < MAX_CLIENT; ++i) {
threads.emplace_back(std::thread{request_server, i});
}

for(auto& p : threads) {
p.join();
}

return 0;
}

编译运行一下:

g++ server.cc -o server -std=c++11 -I /usr/include/ -lpthread
g++ client.cc -o client -std=c++11 -I /usr/include/ -lpthread
./server & # 如果想在服务端看到客户端的内容开两个窗口,加了&表示进程后台运行
./client

之后的所有讨论都是基于以上这两段server和client端的代码。

客户端的main函数里,客户端发送1000个请求,分别使用for循环和多线程,观察二者的耗时: 在这里插入图片描述 可以发现多线程访问仅仅比for循环快了一点点,这是因为服务器是单线程的,处理客户端连接和处理逻辑都在一个线程中,并且read是阻塞的,如果客户端的数据一直不到,那么服务端线程就等在read,无法继续执行。 请添加图片描述

3.2 多线程阻塞服务器

【代码:reactor/multi_thread_sync_block】

你的生意越来越红火,排队的人也越来越多,但是有些人看排队太久吃不上你的煎饼果子就去旁边吃鸡蛋灌饼了,你看着生意被鸡蛋灌饼分走心里不太爽,于是狠狠心月薪2万招聘了一个牛马帮你摊,你的摊子于是有两个人可以同时做煎饼果子,速度一下上去了。

按照煎饼果子的思路,服务器可以把来一个连接就处理一个连接变成来一个连接就创建一个线程去处理这个连接,各个连接的处理互不影响,这样就可以同时响应多个客户端请求了,服务端循环处理连接的代码就可以换成:

while (1)
{
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int connfd = accept(sockfd, (struct sockaddr*)&cli_addr, &cli_len);

std::thread t(handle_request, connfd);
t.detach();
}

看一下1000个线程客户端请求的耗时: 在这里插入图片描述 相比于单线程的服务器,速度快了不是一星半点,其模型如下图: 请添加图片描述 那有了多线程是不是就高枕无忧了呢?服务端想接收多少请求就能接收多少吗?在3.1节最开始提到创建线程是一个开销比较大的操作(这个问题将会在后面讲线程池时进行讲解),默认分配给线程栈的大小就是1MB,那么1024个线程就会占用1GB的内存,因此线程也是非常宝贵的资源,不可能无限扩展。其次如果单个服务端的线程阻塞在read,等待客户端的请求,并且无法继续其他的处理逻辑,处于空闲状态。当有足够多的线程都处于阻塞时,服务端将无法接收更多来自客户端的请求。

那么接下来该怎么做呢?且听下回分解。。。 下一篇链接:多线程服务器分析——Reactor线程模型和性能分析(一) [1]《Linux多线程服务端编程》 [2] 理解一下5种IO模型、阻塞IO和非阻塞IO、同步IO和异步IO

赞(0)
未经允许不得转载:网硕互联帮助中心 » 多线程服务器分析——Reactor线程模型和性能分析(序章)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!