Webserver 02 半同步半反应堆线程池

目录

线程池:

为什么使用线程池?

 怎么创建线程池?

半同步/半反应堆

 五种IO模型

阻塞和非阻塞IO

非阻塞IO和异步IO区别

同步IO和异步IO

事件处理模式Reactor和Proactor模式区别

同步IO模拟proactor模式

多线程中线程越多越好吗

每个线程占多大内存 

如果一个客户请求需要占用线程很久的时间,会不会影响接下来的客户请求呢,有什么好的策略呢?

如果同时1000个客户端进行访问请求,线程数不多,怎么能及时响应处理每一个呢?

线程池工作线程处理完一个任务后的状态是什么

线程池中的工作线程一直是等待的吗

线程池代码分析

线程池中线程数量选择:

静态成员变量

静态成员函数

线程池类的定义:

线程池创建与回收:

对线程进行detach线程分离有什么作用

向请求队列中添加任务


线程池:

1.空间换时间,使用服务器的硬件资源,换取运行效率;

2.池是一组资源的集合,这组资源在服务器启动之初就被完全创建并初始化好,称为静态资源

3.当服务器进入正式运行阶段,开始处理客户请求,如果他需要相关资源可以直接从池中获取,无需动态分配;

4.当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用;

为什么使用线程池?

1. 避免创建和销毁线程所产生的开销,避免活动线程消耗系统的资源

2. 提高响应速度

3. 提高线程的可管理性,使用线程池可以进行统一分配、调优和监控;

使用多线程充分利用多核CPU,并使用线程池避免频繁创建、销毁加大系统开销:

        a)创建一个线程池来管理多线程,线程池中主要包含任务队列工作线程集合,将任务添加到队列中,然后在创建线程后,自动启动这些任务。使用了一个固定线程数的工作线程,限制线程最大并发数;

        b)多个线程共享任务队列,所以需要下线程间同步

 怎么创建线程池?

该项目使用线程池((半同步半反应堆模式))并发处理用户请求,

        主线程负责监听文件描述符,接受socket新的连接,若当前监听的socket发生了读写事件,将任务插到请求队列中。

        工作线程(连接池中的线程)从请求队列中取出任务,完成数据的读写处理,逻辑处理(HTTP请求报文的解析等)。

半同步/半反应堆

半同步/半异步模式:

        既包含同步操作,又包含异步操作,在这种模式下,同步操作和异步操作共同协作,处理并发任务提高效率和可扩展性。

        实现半同步/半异步模式时,通常会使用一个线程池和一个消息队列。同步操作会在主线程中直接执行,而异步操作则会在单独的线程中执行,并将结果放入消息队列中。主线程会从消息队列中读取结果,并进行处理。这样,同步操作和异步操作可以同时进行,提高程序的处理效率。

工作流程:

  • 同步线程用于处理客户逻辑

  • 异步线程用于处理I/O事件

  • 异步线程监听到客户请求后,就将其封装成请求对象并插入请求队列中

  • 请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象

领导者/追随者模式:

        领导者负责系统的状态管理、决策和其他核心任务,追随者节点则通过与领导者节点进行通信,以获得系统状态更新指令。正常情况下,领导者节点宕机或者失去连接,那么系统中的其他节点会自动选举新的领导节点,确保系统的正常运行。

半同步/半反应堆并发模式是半同步/半异步的变体,将半异步具体化为某种事件处理模式

并发模式中的同步和异步:

· 同步指的是程序完全按照代码序列的顺序执行

· 异步指的是程序的执行需要等待特定的事件发生才会执行相应的代码

半同步/半反应堆工作流程(以Proactor模式为例)

  • 主线程充当异步线程,负责监听所有socket上的事件

  • 若有新请求到来,主线程接收之以得到新的连接socket,然后往epoll内核事件表中注册该socket上的读写事件

  • 如果连接socket上有读写事件发生,主线程从socket上接收数据,并将数据封装成请求对象插入到请求队列中

  • 所有工作线程睡眠在请求队列上,当有任务到来时,通过竞争(如互斥锁)获得任务的接管权

 五种IO模型

  • 阻塞IO:调用者调用了某个函数,必须等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作

  • 非阻塞IO:非阻塞等待,每隔一段时间就去检测IO事件是否就绪,没有就绪就可以做其他事。非阻塞I/O执行系统调用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据errno区分这两种情况,对于accept,recv和send,事件未发生时,errno通常被设置成eagain(意味着一个操作暂时无法完成,因此需要等待一段时间后再尝试。)

  • 信号驱动IO:linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO时间就绪,进程收到SIGIO信号,然后处理IO事件。

  • IO复用:linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。知道有数据可读或可写时,才真正调用IO操作函数

  • 异步IO: linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。

注意:阻塞I/O,非阻塞I/O,信号驱动I/O和I/O复用都是同步I/O。

        同步I/O指内核向应用程序通知的是就绪事件,比如只通知有客户端连接,要求用户代码自行执行I/O操作,异步I/O是指内核向应用程序通知的是完成事件,比如读取客户端的数据后才通知应用程序,由内核完成I/O操作。

阻塞和非阻塞IO

         1,阻塞IO:我调用一个函数,这个函数就卡在在这里,整个程序流程不往下走了【休眠sleep】,该函数卡在这里等待一个事情发生,只有这个事情发生了,这个函数才会往下走;这种函数,就认为是阻塞函数;accept();

这种阻塞,并不好,效率很低;一般我们不会用阻塞方式来写服务器程序,效率低 ;

        2. 非阻塞IO :不会被卡住 ,充分利用时间片,执行效率高

        (1)不断的调用accept(),recvfrom()函数来检查有没有数据到来,如果没有,函数会返回一个特殊的错误标记来告诉你,这种标记可能是EWULDBLOCK,也可能是EAGAIN;如果数据没到来,那么这里有机会执行其他函数,但是也得不停的再次调用accept(),recvfrom()来检查数据是否到来,非常累;

        (2)如果数据到来,那么就得卡在这里把数据从内核缓冲区复制到用户缓冲区,所以复制这个阶段是卡着完成的;

非阻塞IO和异步IO区别

        两者都是为了提高IO效率和性能提出的解决方案,非阻塞IO是指在进行IO操作的过程中,如果没有数据可读或可写不会一直等待,而是立即返回,但是它会在定时轮询会浪费CPU资源。    

        异步IO则是指在进行IO操作时,不需要等待操作完成,可以继续执行其他的操作,当IO操作完成后再通知应用程序。这样可以充分利用CPU资源,提高系统的吞吐量和并发性能。但是,异步IO需要操作系统提供支持,并且编程复杂度较高。

        综上所述,非阻塞IO适用于IO操作比较简单、并发量不太高的场景,而异步IO适用于IO操作比较复杂、并发量较高的场景。

同步IO和异步IO

       1. 异步IO :调用一个异步IO函数时,我们要给这个函数指定一个接收缓冲区,还要给定一个回调函数,调用完一个异步IO函数后,该函数会立即返回。其余判断交给操作系统,操作系统会判断数据是否到来,如果数据到来了,操作系统会把数据拷贝到你所提供的缓冲区中,然后调用你所指定的这个回调函数来通知你。

        2. 同步IO:调用select()判断有没有数据,有数据,走下来,没数据卡在那里;select()返回之后,用recvfrom()去取数据;当然取数据的时候也会卡那么一下;

        总的来说,同步IO适合在处理小量数据或者单个请求时使用,而异步IO适合在处理大量数据或者高并发请求时使用,可以提高程序的性能和吞吐量。

事件处理模式Reactor和Proactor模式区别

  • reactor模式中,主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话立即通知工作线程(逻辑单元 ),将socket可读事件放入请求队列,交给工作线程处理。除此之外,主线程不做任何其他性质得工作,交给工作线程处理。 除此之外,读写数据、接受新连接及处理客户请求均在工作线程中完成,通常由同步I/O实现。

  • proactor模式中,主线程和内核负责处理读写数据、接受新连接等I/O操作,工作线程仅负责业务逻辑,如处理客户请求。通常由异步I/O实现。

同步IO模拟proactor模式

由于异步IO并不成熟,实际中使用比较少,这里将使用同步IO模拟实现proactor模式

同步IO模型得工作流程如下:(epoll_wait)为例

         · 主线程往epoll内核事件表注册socket上的读就绪事件;

         ` 主线程调用epoll_wait等待socket上有数据可读

         · 当socket上有数据可读,epoll_wait通知主线程,主线程从socket循环读取数据,直到没有更多数据可读,然后将读取数据封装成一个请求对象插入请求队列

         · 睡眠在请求队列上某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核时间表中注册该socket上的写事件就绪事件

        · 主线程调用epoll_wait等待socket可写

        · 当socket上有数据可写,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。

多线程中线程越多越好吗

不是。

  1. 如果8个CPU,8个线程,每个线程占用一个CPU,同一时间,八个线程都往前跑,效率更搞
  2. 如果此时有9个线程 ,同时运行就牵扯到线程切换
  3. 随着线程的增多,效率越来越高,但是到了一个峰值,再增加就会出现问题,线程太多需要来回切换,可能用于切换的时间比执行时间还长,这就不合适了

每个线程占多大内存 

        每个线程的内存占用量取决于线程所分配的堆栈空间和使用的堆空间,一般情况下,每个线程的内存占用量较小,大约在几百KB到几MB之间。但是,如果线程需要操作大量数据或者进行复杂的计算,其内存占用量可能会增大。

如果一个客户请求需要占用线程很久的时间,会不会影响接下来的客户请求呢,有什么好的策略呢?

        会,因为线程池内线程的数量时有限的,如果客户请求占用线程时间过久的话会影响到处理请求的效率,当请求处理过慢时会造成后续接受的请求只能在请求队列中等待被处理,从而影响接下来的客户请求。

        我们可以为线程处理请求对象设置处理超时时间, 超过时间先发送信号告知线程处理超时,然后设定一个时间间隔再次检测,若此时这个请求还占用线程则直接将其断开连接。

如果同时1000个客户端进行访问请求,线程数不多,怎么能及时响应处理每一个呢?

        本项目是通过对子线程循环调用来解决高并发的问题的。

        首先在创建线程的同时就调用了pthread_detach将线程进行分离,不用单独对工作线程进行回收,资源自动回收。

        我们通过子线程的run调用函数进行while循环,让每一个线程池中的线程永远都不会停止,访问请求被封装到请求队列(list)中,如果没有任务线程就一直阻塞等待,有任务线程就抢占式进行处理,直到请求队列为空,表示任务全部处理完成。

线程池工作线程处理完一个任务后的状态是什么

(1) 当处理完任务后如果请求队列为空时,则这个线程重新回到阻塞等待的状态

(2) 当处理完任务后如果请求队列不为空时,那么这个线程将处于与其他线程竞争资源的状态,谁获得锁谁就获得了处理事件的资格。

线程池中的工作线程一直是等待的吗

        线程池中的工作线程是处于一直阻塞等待状态下的。因为我们创建线程池之初时,通过循环调用pthread_create往线程池中创建8个线程,工作线程处理函数接口为pthread_create函数中第三个参数指针指向的自定义worker函数,然后子线程调用线程池类中成员函数run自定义函数执行process函数任务运行。

        worker必须是一个静态的函数,由于静态成员函数只能访问静态成员变量,所以为了能够访问到类内非静态成员变量,只能通过在worker中调用run函数这个非静态成员变量来达到这一要求。

        在run函数中,我们为了能够处理高并发的问题,将线程池中的工作线程都设置为阻塞等待在请求队列是否不为空的条件上,因此项目中线程池中的工作线程是处于一直阻塞等待的模式下的。

线程池代码分析

主线程 为异步线程=》监听文件描述符(接收socket新的连接以及监听socket读写事件并将数据封装成一个请求对象插入请求队列)

工作线程 从请求队列种取出任务 完成读写数据处理。

线程池中线程数量选择:

8个 因为我是八核CPU的

        调整这个线程池中线程数量主要目的地是为了充分合理的使用CPU和内存资源,从而最大限度的提高程序的性能。

        如果是CPU密集型任务,尽量压榨CPU,设置位Ncpu+1 ,(+1 是保证当线程由于页缺失故障(操作系统)或其它原因 导致暂停时,额外的这个线程就能顶上去,保证CPU 时钟周期不被浪费)

        如果是IO密集型任务,参考值可以设置为 2 * NcpuIO的处理一般较慢,多于cores数的线程将为CPU争取更多的任务,不至在线程处理IO的过程造成CPU空闲导致资源浪费

静态成员变量

       将类成员变量声明为static,则为静态成员变量,与一般的成员变量不同,无论建立多少对象,都只有一个静态成员变量的拷贝,静态成员变量属于一个类,所有对象共享。

        静态变量在编译阶段就分配了空间,对象还没创建时就已经分配了空间,放到全局静态区。

静态成员函数

将类成员函数声明为static,则为静态成员函数。

  • 静态成员函数可以直接访问静态成员变量,不能直接访问普通成员变量,但可以通过参数传递的方式访问。

  • 普通成员函数可以访问普通成员变量,也可以访问静态成员变量。

  • 静态成员函数没有this指针。非静态数据成员为对象单独维护,但静态成员函数为共享函数,无法区分是哪个对象,因此不能直接访问普通变量成员,也没有this指针。

线程池类的定义:

        线程处理函数和运行函数设置为私有函数,可以保证只有线程池类内部可以访问和修改,避免外部程序的干扰,提高了代码的可靠性和安全性。

//线程池类定义
template <typename T>
class threadpool
{
public:
    /*connPool是数据库连接池指针
    thread_number是线程池中线程的数量,
    max_requests是请求队列中最多允许的、等待处理的请求的数量*/
    threadpool(connection_pool *connPool, int thread_number = 8, int max_request = 10000);
    ~threadpool();
    bool append(T *request);
private:
    /*工作线程运行的函数,它不断从工作队列中取出任务并执行之*/
    static void *worker(void *arg);
    void run();
private:
    int m_thread_number;        //线程池中的线程数
    int m_max_requests;         //请求队列中允许的最大请求数
    pthread_t *m_threads;       //描述线程池的数组,其大小为m_thread_number
    std::list<T *> m_workqueue; //请求队列
    locker m_queuelocker;       //保护请求队列的互斥锁
    sem m_queuestat;            //是否有任务需要处理
    bool m_stop;                //是否结束线程
    connection_pool *m_connPool;  //数据库连接池 
};

线程池创建与回收:

        构造函数种创建线程池,pthread_create函数中将类的对象作为参数传递给静态函数(work) 在静态函数中引用这个对象,并调用run函数

//线程处理函数
template <typename T>
void *threadpool<T>::worker(void *arg)
{
    //将参数强转为线程池类 调用成员方法 
    threadpool *pool = (threadpool *)arg;
    pool->run();
    return pool;
}
//执行任务 
template <typename T>
void threadpool<T>::run()
{
    while (!m_stop)
    {
        //信号量 等待 
        m_queuestat.wait();

        m_queuelocker.lock();
        if (m_workqueue.empty())
        {
            m_queuelocker.unlock();
            continue;
        }

        //从请求中取出第一个任务
        //将任务从请求队列删除 
        T *request = m_workqueue.front();
        m_workqueue.pop_front();
        m_queuelocker.unlock();
        if (!request)
            continue;

        connectionRAII mysqlcon(&request->mysql, m_connPool);
        
        request->process();
    }
}
//线程池创建与回收

template <typename T>
threadpool<T>::threadpool( connection_pool *connPool, int thread_number, int max_requests) : m_thread_number(thread_number), m_max_requests(max_requests), m_stop(false), m_threads(NULL),m_connPool(connPool)
{
    if (thread_number <= 0 || max_requests <= 0)
        throw std::exception();

    //线程id初始化
    m_threads = new pthread_t[m_thread_number];
    if (!m_threads)
        throw std::exception();
    for (int i = 0; i < thread_number; ++i)
    {
        //循环创建线程 并将工作线程按要求进行运行
        //printf("create the %dth thread\n",i);
        if (pthread_create(m_threads + i, NULL, worker, this) != 0)
        {
            delete[] m_threads;
            throw std::exception();
        }
        //将线程进行线程分离 不用单独对工作线程进行回收
        if (pthread_detach(m_threads[i]))
        {
            delete[] m_threads;
            throw std::exception();
        }
    }
}

对线程进行detach线程分离有什么作用

作用是将该线程和主线程分开,两者可以并行执行,互不影响。

        1. 后台执行:通过将线程设置为分离线程,可以让其运行在后台,不影响主线程继续运行,在处理一些耗时的任务或需要长时间执行的才做非常有用,以避免主线程在等待线程完成时阻塞。

        2. 资源回收:分离的线程在执行完毕后,可以自动释放其占用的资源,包括内存、文件句柄等。不需要显式的等待线程结束并回收线程, 可以避免资源泄露。

        3. 线程生命周期的管理:分离线程的生命周期不在由主线程控制,它可以独立运行直至结束,不受抓线程的影响。

向请求队列中添加任务

        通过list容器创建请求队列,向队列中添加时,通过互斥锁保证线程安全,添加完成后通过信号量提醒有任务要处理,最后注意线程同步。

        请求队列可以通过(数组、链表、队列、栈等容器创建)链表可以动态的添加或删除元素

//向请求队列中添加任务

template <typename T>
bool threadpool<T>::append(T *request)
{
    m_queuelocker.lock();

    //根据硬件,预先设置请求队列的最大值 10000
    if (m_workqueue.size() > m_max_requests)
    {
        m_queuelocker.unlock();
        return false;
    }
    //添加任务
    m_workqueue.push_back(request);
    m_queuelocker.unlock();

    //信号量提醒有任务要处理
    m_queuestat.post();
    return true;
}

引用\[1\]和\[2\]提供了关于线程池的实现细节,包括创建多个线程、向工作队列添加任务、线程池的启动和销毁等。线程池是一种用于管理和复用线程的技术,可以提高多线程程序的性能和效率。 引用\[3\]提供了关于同步与异步、阻塞与非阻塞的解释。同步和异步强调的是消息通信机制,阻塞和非阻塞强调的是程序在等待调用结果时的状态。在webserver线程池可以采用同步I/O模拟proactor模式,主线程负责监听事件,工作线程负责处理客户请求。 综上所述,webserver线程池是一种通过管理和复用线程来提高性能和效率的技术。它可以实现同步或异步的消息通信机制,并可以采用阻塞或非阻塞的方式处理请求。 #### 引用[.reference_title] - *1* *3* [webserver-线程池同步机制类以及线程池实现](https://blog.csdn.net/weixin_44654302/article/details/128093190)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [手把手实现webserver网页服务器(二)-- 线程池的实现](https://blog.csdn.net/twopq/article/details/122428412)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值