Highly concurrent epoll+ thread pool, thread pool focuses on realizing business

Share a few videos that I listened to at bilibili and thought they were good:

1. Handwriting thread pool video : https://www.bilibili.com/video/BV1AT4y13791

2. Video explaining the principle of epoll : https://www.bilibili.com/video/BV1Xr4y1c7aV

3. Handwritten user mode protocol stack video : https://www.bilibili.com/video/BV1x54y1t7so

As we know, the server concurrency model can usually be divided into single-threaded and multi-threaded models. The thread here usually refers to the "I/O thread", that is, the "management thread" responsible for I/O operations and coordinating and assigning tasks. The actual request And tasks are usually handled by so-called "worker threads". Usually under the multi-threaded model, each thread is both an I/O thread and a worker thread. So what is discussed here is the single I/O thread + multi-worker thread model, which is also the most commonly used server concurrency model. This model can be seen everywhere in the server code of my project. It also has a name called the "semi-synchronous/semi-asynchronous" model. At the same time, this model is also a manifestation of the producer/consumer (especially multi-consumer) model.

This architecture is mainly based on the idea of ​​I/O multiplexing (mainly epoll, select/poll is outdated). Through single-threaded I/O multiplexing, efficient concurrency can be achieved while avoiding multi-threaded I/ The various overheads of switching back and forth are clear and easy to manage, and multi-worker threads based on thread pools can give full play to and use the advantages of multi-threading, and use thread pools to further improve resource reuse and avoid excessive production Thread.

The bottleneck is IO intensity. In the thread pool, you can open up 10 threads, of course, all accept block, so that the client will automatically activate a thread to process when it comes up, but imagine that if all 10 threads are used up, the 11th client will be A discard occurred. In order to achieve "high concurrency" you have to increase the number of thread pools. This will cause serious memory usage and thread switching delay problems. So the plan of pre-event polling facility came into being, the main thread polling is responsible for IO, and the job is handed over to the thread pool. Under high concurrency, when 10W clients come up, the main thread is responsible for accepting, and they are placed in the queue, so that the connection will not be discarded without a timely handshake, and the job thread will claim the job from the queue and reply to the main thread after completion. , The main thread is responsible for write. This can handle a large number of connections with very few system resources. Under low concurrency, for example, if two clients come up, there will not be a situation where 100 threads hold live there, resulting in a waste of system resources.

Correctly implement the core of the basic thread pool model: The main thread is responsible for all I/O operations, and if necessary, after collecting all the data for a request, it is handed over to the worker thread for processing. After the processing is completed, return the data that needs to be written back to the main thread for writing back/try to write back the data until it is blocked, and then hand it back to the main thread to continue. Here "if necessary" means: after measurement, it is confirmed that the CPU time consumed during this processing (not including any I/O waiting, or related I/O waiting operations cannot be taken over by epoll) is quite significant. If this process (not including I/O operations that can be taken over) is not significant, it can be solved directly in the main thread. The premise of this "necessary" is only three words: hypothesis, analysis, and measurement.

Therefore, for a correctly implemented thread pool environment clock, the advantage of using epoll + non-blocking I/O instead of select + blocking I/O is that when processing a large number of sockets, the former is more efficient than the latter, because the former does not require every time After being awakened, recheck all fd to determine which fd status changes can be read and written.

The essential

1. Single I/O thread epoll

The epoll model that implements a single I/O thread is the first technical point of this architecture. The main ideas are as follows:

A single thread creates epoll and waits. When an I/O request (socket) arrives, it is added to epoll and an idle worker thread is taken from the thread pool, and the actual business is handled by the worker thread.

Pseudo code:

创建一个epoll实例;
while(server running)
{
    epoll等待事件;
    if(新连接到达且是有效连接)
    {
        accept此连接;
        将此连接设置为non-blocking;
       为此连接设置event(EPOLLIN | EPOLLET ...);
        将此连接加入epoll监听队列;
        从线程池取一个空闲工作者线程并处理此连接;
    }
    else if(读请求)
    {
        从线程池取一个空闲工作者线程并处理读请求;
    }
    else if(写请求)
    {
        从线程池取一个空闲工作者线程并处理写请求;
    }
    else
        其他事件;     
} 

 

More exciting content for C/C++Linux server development includes: C/C++, Linux, Nginx, ZeroMQ, MySQL, Redis, MongoDB, ZK, streaming media, P2P, Linux kernel, Docker, TCP/IP, coroutine, DPDK Sharing of multiple advanced knowledge points. Click the link to subscribe and watch it directly: https://ke.qq.com/course/417774?flowToken=1013189

Video learning materials + group 720209036 acquisition

 

2. Thread pool implementation

When the server is started, a certain number of worker threads are created to join the thread pool, such as (20), for I/O threads to fetch;

Whenever an I/O thread requests an idle worker thread, an idle worker thread is taken from the pool to process the corresponding request;

When the request is processed and the corresponding I/O connection is closed, the corresponding thread is recycled and returned to the thread pool for next use;

If there is no idle worker thread when the idle worker thread pool is requested, the following processing can be done:

(1) If the total number of threads "managed" in the pool does not exceed the maximum allowable value, a new batch of worker threads can be created to join the pool, and one of them will be returned for use by the I/O thread;

(2) If the total number of threads "managed" in the pool has reached the maximum value, no more new threads should be created. Wait for a short period of time and try again. Note that because the I/O thread is a single thread and should not be blocked waiting here, the management of the thread pool should be completed by a dedicated management thread, including the creation of new worker threads. At this time, the management thread is blocked and waiting (such as using a condition variable and waiting to be awakened). After a short period of time, there should be idle worker threads in the thread pool. Otherwise, the server load is estimated to be a problem.

Epoll is a perfect solution for high-concurrency servers under Linux. Because it is triggered based on events, it is not only an order of magnitude faster than select.

Single-threaded epoll, the trigger volume can reach 15000, but after adding the business, because most of the business deals with the database, there will be a blocking situation, this time you must use multiple threads to speed up.

The business is in the thread pool, so locks are needed here. 2300 test results/s

Test tool: stressmark

Because the code suitable for ab is added, ab can also be used for stress testing.

char buf[1000] = {0};

sprintf(buf,"HTTP/1.0 200 OK\r\nContent-type: text/plain\r\n\r\n%s","Hello world!\n");

send(socketfd,buf, strlen(buf),0);

#include <iostream>

#include <sys/socket.h>

#include <sys/epoll.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <fcntl.h>

#include <unistd.h>

#include <stdio.h>

#include <pthread.h>

 

#include <errno.h>

  

#define MAXLINE 10

#define OPEN_MAX 100

#define LISTENQ 20

#define SERV_PORT 8006

#define INFTIM 1000

  

//线程池任务队列结构体

 

struct task{

  int fd; //需要读写的文件描述符

 

  struct task *next; //下一个任务

 

};

  

//用于读写两个的两个方面传递参数

 

struct user_data{

  int fd;

  unsigned int n_size;

  char line[MAXLINE];

};

  

//线程的任务函数

 

void * readtask(void *args);

void * writetask(void *args);

  

  

//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件

 

struct epoll_event ev,events[20];

int epfd;

pthread_mutex_t mutex;

pthread_cond_t cond1;

struct task *readhead=NULL,*readtail=NULL,*writehead=NULL;

  

void setnonblocking(int sock)

{

     int opts;

     opts=fcntl(sock,F_GETFL);

     if(opts<0)

     {

          perror("fcntl(sock,GETFL)");

          exit(1);

     }

    opts = opts|O_NONBLOCK;

     if(fcntl(sock,F_SETFL,opts)<0)

     {

          perror("fcntl(sock,SETFL,opts)");

          exit(1);

     }

}

  

int main()

{

     int i, maxi, listenfd, connfd, sockfd,nfds;

     pthread_t tid1,tid2;

     

     struct task *new_task=NULL;

     struct user_data *rdata=NULL;

     socklen_t clilen;

     

     pthread_mutex_init(&mutex,NULL);

     pthread_cond_init(&cond1,NULL);

     //初始化用于读线程池的线程

 

     pthread_create(&tid1,NULL,readtask,NULL);

     pthread_create(&tid2,NULL,readtask,NULL);

     

     //生成用于处理accept的epoll专用的文件描述符

 

     epfd=epoll_create(256);

  

     struct sockaddr_in clientaddr;

     struct sockaddr_in serveraddr;

     listenfd = socket(AF_INET, SOCK_STREAM, 0);

     //把socket设置为非阻塞方式

 

     setnonblocking(listenfd);

     //设置与要处理的事件相关的文件描述符

 

     ev.data.fd=listenfd;

     //设置要处理的事件类型

 

     ev.events=EPOLLIN|EPOLLET;

     //注册epoll事件

 

     epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

     

     bzero(&serveraddr, sizeof(serveraddr));

     serveraddr.sin_family = AF_INET;

     serveraddr.sin_port=htons(SERV_PORT);

     serveraddr.sin_addr.s_addr = INADDR_ANY;

     bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));

     listen(listenfd, LISTENQ);

     

     maxi = 0;

     for ( ; ; ) {

          //等待epoll事件的发生

 

          nfds=epoll_wait(epfd,events,20,500);

          //处理所发生的所有事件

 

        for(i=0;i<nfds;++i)

        {

               if(events[i].data.fd==listenfd)

               {

                    

                    connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);

                    if(connfd<0){

                      perror("connfd<0");

                      exit(1);

                   }

                    setnonblocking(connfd);

                    

                    char *str = inet_ntoa(clientaddr.sin_addr);

                    //std::cout<<"connec_ from >>"<<str<<std::endl;

 

                    //设置用于读操作的文件描述符

 

                    ev.data.fd=connfd;

                    //设置用于注测的读操作事件

 

                 ev.events=EPOLLIN|EPOLLET;

                    //注册ev

 

                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);

               }

            else if(events[i].events&EPOLLIN)

            {

                    //printf("reading!/n");

 

                    if ( (sockfd = events[i].data.fd) < 0) continue;

                    new_task=new task();

                    new_task->fd=sockfd;

                    new_task->next=NULL;

                    //添加新的读任务

 

                    pthread_mutex_lock(&mutex);

                    if(readhead==NULL)

                    {

                      readhead=new_task;

                      readtail=new_task;

                    }

                    else

                    {

                     readtail->next=new_task;

                      readtail=new_task;

                    }

                   //唤醒所有等待cond1条件的线程

 

                    pthread_cond_broadcast(&cond1);

                    pthread_mutex_unlock(&mutex);

              }

               else if(events[i].events&EPOLLOUT)

               {

                 /*

              rdata=(struct user_data *)events[i].data.ptr;

                 sockfd = rdata->fd;

                 write(sockfd, rdata->line, rdata->n_size);

                 delete rdata;

                 //设置用于读操作的文件描述符

                 ev.data.fd=sockfd;

                 //设置用于注测的读操作事件

               ev.events=EPOLLIN|EPOLLET;

                 //修改sockfd上要处理的事件为EPOLIN

               epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

             */

               }

                              

          }

          

     }

}

 

static int count111 = 0;

static time_t oldtime = 0, nowtime = 0;

void * readtask(void *args)

{

    

   int fd=-1;

   unsigned int n;

   //用于把读出来的数据传递出去

 

   struct user_data *data = NULL;

   while(1){

         

        pthread_mutex_lock(&mutex);

        //等待到任务队列不为空

 

        while(readhead==NULL)

             pthread_cond_wait(&cond1,&mutex);

         

        fd=readhead->fd;

        //从任务队列取出一个读任务

 

        struct task *tmp=readhead;

        readhead = readhead->next;

        delete tmp;

        pthread_mutex_unlock(&mutex);

        data = new user_data();

        data->fd=fd;

         

 

        char recvBuf[1024] = {0};

        int ret = 999;

        int rs = 1;

 

        while(rs)

        {

            ret = recv(fd,recvBuf,1024,0);// 接受客户端消息

 

            if(ret < 0)

            {

                //由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可//读在这里就当作是该次事件已处理过。

 

                if(errno == EAGAIN)

                {

                    printf("EAGAIN\n");

                    break;

                }

                else{

                    printf("recv error!\n");

         

                    close(fd);

                    break;

                }

            }

            else if(ret == 0)

            {

                // 这里表示对端的socket已正常关闭.

 

                rs = 0;

            }

            if(ret == sizeof(recvBuf))

                rs = 1; // 需要再次读取

 

            else

                rs = 0;

        }

        if(ret>0){

 

        //-------------------------------------------------------------------------------

 

 

            data->n_size=n;

 

 

            count111 ++;

 

            struct tm *today;

            time_t ltime;

            time( &nowtime );

 

            if(nowtime != oldtime){

                printf("%d\n", count111);

                oldtime = nowtime;

                count111 = 0;

            }

 

            char buf[1000] = {0};

            sprintf(buf,"HTTP/1.0 200 OK\r\nContent-type: text/plain\r\n\r\n%s","Hello world!\n");

            send(fd,buf,strlen(buf),0);

            close(fd);

 

 

       }

   }

}
 

 

Guess you like

Origin blog.csdn.net/Linuxhus/article/details/112593824