多线程并发http任务的设计

多线程并发http任务的设计

目录

多线程并发http任务的设计

一、整体架构

1.1、服务端的系统资源限制条件 

1.2、服务端的应用服务的主动适配

1.3、客户端的http客户端连接的主动适配

二、客户端连接池、线程池的设计

2.1、实际留给我们的”运行时“的”并发线程数“

2.2、客户端连接池、线程池的设计方案

2.2.1、在单个线程中循环“http异步请求”

2.2.2、在计时器轮询状态的循环中实现多线程的“异步并发请求”

喜欢的,就收藏并点个赞,鼓励我继续技术的原创写作及经验分享:


一、整体架构

1.1、服务端的系统资源限制条件 

       在分析和设计本案时,先了解和明确一些基本概念:

1.1.1、单个物理服务器  

1.1.1.1、基本概念

        每个计算机,其硬件设计阶段,就决定了其物理资源:CPU性能参数、内存I/O、磁盘I/O等。

        应用程序的进程: 操作系统统一管理和调度的应用程序的“服务”、“模块”和“句柄”。

        进程内线程的用户态: 用户应用程序中定义和执行的线程。

        进程内线程的系统态:物理设备的CPU及其操作系统,统一管理所有“用户态”线程及其调度。

        设备有几路物理CPU:即主板上Socket内的插槽数

        单路物理CPU的核心数core:该路CPU的核心

        通常所说的多处理器: 就是指设备的“逻辑处理器”。

        逻辑处理器可处理的线程数: 能且只能处理2个线程的队列。

        单位时间内:CPU的每逻辑处理器,能切只能处理1个线程Worker,即只能干1个活儿;最多2个工作线程压入栈,后进先出;栈内,后入的干完活儿后弹出,栈底的第2个开始入队处理后弹出。

        单个物理服务器最多能并发多少个线程的执行:

                公式 = 逻辑CPU总数 * 2

               比如上图的8核服务器,最多并发16个线程。


var
  /// <summary>◆本机Windows平台默认线程池最大的工作线程数:<para></para></summary>
  MaxLimitWorkerThreadCount:Integer;
  /// <summary>◆本机默认连接池每个服务器的最大tcp并发连接数:<para></para></summary>
  MaxConnectionsPerServer: Integer;
  /// <summary>◆本机最大的并发http请求任务数:<para></para></summary>
  MaxIFutureTask: Integer;
var
  /// <summary>◆全局未来函数完成Rest和本地的同步Json的数据管理:<para>TKey,TValue</para></summary>
  FDictionary_IFuture:TDictionary<string,IFuture<string>>=nil;
var
  /// <summary>◆平台默认的连接池的最大连接数的默认值:<para>适用win7、win10、win11客户端注册表默认值</para></summary>
  FConnectsPoolMaxCount: Integer =10;

  /// <summary>◆连接池中的THTTPClient连接对象字典:<para></para></summary>
  FConnectsManager_ObjDict: TObjectDictionary<string,THTTPClient>=nil;

  /// <summary>◆有了对象字典FConnectsManager_ObjDict可以转化为数组.TArray,可以不再单独设定数组的键值对:<para>备用,暂时不用</para></summary>
  //FConnectsManager_ObjDictArray: TArray<TPair<string,THTTPClient>>=nil;//连接池中的THTTPClient连接对象的数组键值对

  /// <summary>◆暂不使用,至少4个才可枚举,连接池中的http客户端的连接状态字典:<para>Status状态字符串:</para>TTaskStatus = (Created, WaitingToRun, Running, Completed, WaitingForChildren, Canceled, Exception)</summary>
  FEnumConnectsStatus: TDictionary<string,string>=nil;

  /// <summary>◆连接池中的连接执行http的异步执行结果的字典:<para>errStatus= 'Completed';创建时'Created'</para><para>键值对说明:aHTTPRequest.Name,IAsyncResultStatus</para></summary>
  FAppHttpConntsAsyncResults:TDictionary<string,string>=nil;

  MaxLimitWorkerThreadCount :=
    TThreadPoolStats.Get( TThreadPool.Default ).MaxLimitWorkerThreadCount;
  //:若该设备是8核逻辑处理器*2=16; 运行时,系统态,会自动减1个线程,至少预留1个可工作线程给其它工作使用,以保持线程间的连续切换
  //——换句话说总是应当定义的并发请求总数最多= MaxLimitWorkerThreadCount -1 = 我这个机器8核心*2 -1 =15
  HttpFilter := THttp_Filters_HttpBaseProtocolFilter.Create; //:Windows平台每服务器最大tcp并发连接数——过滤器——可读可写
    //HttpFilter.MaxConnectionsPerServer := 128;//:http及tcp协议规定的范围[ 2...128 ]

    //:可读写,操作系统默认=6————不要去改它——改了也不稳定
  MaxConnectionsPerServer := HttpFilter.MaxConnectionsPerServer;
  // 赋值系统全局变量-当前平台最大的并发http请求任务数:
  FConnectsPoolMaxCount := MaxLimitWorkerThreadCount;
  // 赋值系统全局变量-最大的并发未来任务的请求数——取小:
  if MaxLimitWorkerThreadCount > MaxConnectionsPerServer then
    MaxIFutureTask := MaxConnectionsPerServer
  else MaxIFutureTask := MaxLimitWorkerThreadCount;

        运行gpedit.msc本地用户组 策略编辑器:

        如上,该设备:

        当前平台默认线程池的最大工作线程: 16
        当前平台默认连接池每个服务器的最大tcp并发连接数: 6
        当前平台最大的并发http请求任务数: 6

        适用范围:服务器设备、客户端设备,均适用。

1.1.1.2、平台默认连接池每个服务器的最大tcp并发连接数

        如上,有朋友会问,为啥当前平台默认连接池每个服务器的最大tcp并发连接数,被设计为6。

       原本,底层TCP协议最初,是不限制的;后来,随着html的发展,http协议诞生了,伴随其上层协议http的应用和推广,tcp标准,开始修改,以适配http的需要。最初,微软的IE浏览器打垮Nescape网景后,在IE8之前适配了http1.0协议的4个;之后http1.1协议适配2个;共6个:

       它的上限,就是注册表编辑器中的10个(适用win7、Win10、Win11):

       运行regedit:

       HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings

        上限=10,适用于适用win7、Win10、Win11,即是说,当你的客户端应用,同时并发了10个http请求,线程不会出现“不安全”的事件。这个数值10,也是并发压测时,同步刷新UI的上限,超出这个数值,UI界面,在线程的同步函数中,是刷新不过来的,必须当前线程稍作内旋暂停后再回到该线程,即:每执行10次并发请求,刷新一次界面,否则刷新不过来。

        之后,微软在.NET Core 3.0后又做了一次调整,客制化的范围从[ 2...128 ]调整为[ 2...256 ]。我们共同的鼻主:Delphi编译器、C#、TS之父、.Net战略的主要实施者,安德斯•海尔斯伯格,在制定标准时,在其中起了重要作用。以上,不仅原生App开发领域要遵循,Html前端领域一样要遵循。主流浏览器,也都自觉遵循了这个”不是标准“的潜规则标准。

        同时还支持了Http2.0协议,并新增了SocketsHttpHandler.EnableMultipleHttp2Connections属性,用于指示在所有现有连接上达到最大并发流数时,是否可以在同一服务器上建立其他 HTTP/2 连接。

        详见微软官方文档:

HttpClientHandler.MaxConnectionsPerServer 属性 (System.Net.Http) | Microsoft Learn

SocketsHttpHandler.PooledConnectionIdleTimeout 属性 (System.Net.Http) | Microsoft Learn

SocketsHttpHandler 类 (System.Net.Http) | Microsoft Learn

1.1.1.2、连接空闲超时

        平台默认值 = 15分钟:

1.1.2、物理服务器的负载均衡与集群

        当单个物理服务器,已经不能安全有效承载来自不同“客户端的并发请求”时,就需要“负载均衡”和服务器集群技术,即我们行话“堆服务器”。

        如上图的实例,其中,IP的mask应当设计为http请求“物理地域”就近的原则。

        比如221.237.0.0/16,适配"中国电信"的成都市武侯区的客户端请求。

1.2、服务端的应用服务的主动适配

// 配置“会话超时”的时间:
SessionOptions.SessionTimeout=15(分钟计量,操作系统默认=15)
ASession.SessionTimeOut:=20(可改写,默认范围[1,20])

// “会话超时”后,连接“断开”,但并未从服务器连接池中清除即“注销”:
// 配置活动但空闲的会话,将其从服务器连接池中注销的超时时间:
SessionOptions.LockSessionTimeout:=60*1000;//1分钟(毫秒计量,操作系统默认=不限制分钟数,一般3分钟)

本地组策略编辑器-计算机配置-管理模板-Windows组件-Internet Explorer-安全功能-AJAX

1.3、客户端的http客户端连接的主动适配

         System.Net.URLClient超时默认值 = 60秒 = 1分钟,与上面服务端60*1000的锁会话超时匹配:

  TURLClient = class
  public const
    DefaultConnectionTimeout = 60000;
    DefaultSendTimeout = 60000;
    DefaultResponseTimeout = 60000;

二、客户端连接池、线程池的设计

       如上所述,其实,操作系统,并为限制你对系统tcp连接上限数值的设计,但事实上在运行时,你总是被限制了。

       这是为什么呢?

       原因是,为了不阻塞UI界面,给客户提供更好的UE体验,我们的http并发请求,总是会放入”用户态线程“中去设计与运行。

       而正如本文1.1.1.1所述,单个计算机最多能并发多少个线程的执行(公式 = 逻辑CPU总数 * 2)是由硬件本身所决定的,不是你的代码所决定的。比如本文1.1中的示例计算机的配置,1台客户端的8核逻辑处理器,最多并发16个线程,且事实上要预留1个逻辑处理器,来处理当用户应用的优先级把你的App切换到”后台“时,这个预留的逻辑处理器能够很好的切换到其它线程。

       那么,实际留给我们的”运行时“的”并发线程数“ = 16 - 1*2 = 14。

       假设,服务器能够“胜任”来自客户端的“并发请求”,那么。客户端,该如何设计呢?

2.1、实际留给我们的”运行时“的”并发线程数“

       公式 = (计算机逻辑处理器总数-1) * 2 。

2.2、客户端连接池、线程池的设计方案

       我们该如何充分利用“实际留给我们的'运行时'的并发线程数”呢?!

2.2.1、在单个线程中循环“http异步请求”

2.2.1.1、以“阻塞式”的“http同步请求”模式实现“异步并发请求”

       “http同步请求”,总是“阻塞式”的通讯,那么,是“http异步请求”好,还是“http同步请求”好?各有优劣。

        “阻塞式”的通讯的杰出代表,跨平台Indy开源通讯库,早在html还在婴幼儿时代,它就被广泛用于全球各行各业、特别的是IT软件开发业态。

        类似Indy“阻塞式”的通讯,代码写起来非常舒服,多线程嵌套时、或线程需要队列时,间接明了、不拖泥带水。

        “阻塞式”的通讯,只要你把它放入“线程”中去运行“通讯”请求,它就不会“阻塞”了。大致的设计和代码书写就像这样“队列化”运行:

// ... 
if aTThread1.Running then aTThread1.aHttpRequest1.Exec;
if aTThread1.Finished if aTThread2.Running then aTThread2.aHttpRequest2.Exec;
// ... 

        或者像这样“并发”运行:

// ... 
if aTThread1.Running then aTThread1.aHttpRequest1.Exec;
if aTThread2.Running then aTThread2.aHttpRequest2.Exec;
// ... 

2.2.1.2、或者类似这样在单线程中,以通用的代码,“并发”运行各个“异步请求”

// ...事前,定义好异步请求的字典httpASyncRequests_TDictionary,然后,从连接池中随机调用一个空闲的aHttpClient连接,来发送请求:
if httpASyncRequests_TDictionary.Count >0 then
TThread.CreateAnonymousThread( 
  procedure var loop:Integer;
  begin // IFutures_TDictionary
    for loop:=0 to httpASyncRequests_TDictionary.Count-1 do
    begin
      try
        try
          if Length(httpASyncRequests_TDictionary.ToArray) > loop then
          begin
            if httpASyncRequests_TDictionary.ToArray[loop].Key<>'' then
            begin
              TThread.Synchronize(TThread.Current,procedure begin
                aHttpClient :=
                  Extract1RandomIdleTHttpClient( ifExistsRandomIdleTHttpClient() );
                //:从连接池中随机调用一个空闲的aHttpClient连接
              end);
              //...这里省略了不少代码————意在判断请求的资源在本地持久的状态
              //   ——以使得计时器能反复产生新的单线程,完成没有完成的Worker
              aIRequest:= aHttpClient.GetRequest( 
                httpASyncRequests_TDictionary.ToArray[loop].Value as THttpClient );
              aHTTPResponse :=
                aHttpClient.Execute( aIRequest, nil,aIRequest.Headers ) as IHTTPResponse;
            end;
          end;
        except
          //...
        end;
      finally
        if aHTTPResponse<>nil then
        begin
          errStatus :=  aHTTPResponse.ContentAsString( TEncoding.UTF8 );
          TThread.Synchronize(TThread.Current,
          procedure begin 
            LogMe( httpASyncRequests_TDictionary.ToArray[loop].Key.Name
              +'执行结果Json——' + errStatus ); 
          end);
        end;
      end;
      if (loop<>0) then
      begin
        Application.ProcessMessages; //:不卡外层的UI——外层监听在主线程
        Sleep(50);//:循环不要太快了————否则——最底层的tcp连接繁忙————也可分不同请求差异化配置
      end;
    end;
  end;
).start;

       我们知道,http的底层是tcp,tcp创建连接时非常耗时的。

       某个THttpClient连接类实例,只要它归属的作用域为Application应用程序,那么它的生命周期就可以设计为以下其中1个:

       ◆ “超时,才从连接池中自动被销毁”;

       ◆“被应用程序退出所彻底销毁”。

       而不必设计为“用完即销毁”;只要它没有被销毁,那么本机的“连接池”中,这个Http客户端就可以拿来被“复用”,从而提高“网络”效率。

2.2.1.3、如何甄别不同的http客户端连接

2.2.2、在计时器轮询状态的循环中实现多线程的“异步并发请求”

       为了同时适配:本机嵌入式浏览器“多标签页”或其“后台js worker专用线程”的需要,我们还可以“异步请求”的非阻塞方式,来实现并发线程的“异步并发请求”。


type
  /// <summary>全局继承类___实例化1个统一的TBasicAction的组件类TComponent<para></para>来统一调度全局计时器的OnTimer事件<para></para></summary>
  TimersOnTimerDownloadDatabase = class(TBasicAction)
  public
    /// <summary>执行全局计时继承类___统一调度执行其OnTimer的目标任务:<para></para></summary>
    procedure ExecuteTarget(Target: TObject);
  end;
/// <summary>全局继承类的实例___TokenOnTimer计时器事件的调度处理器<para></para></summary>
var TimerExec_OnTimerDownloadDatabase : TimersOnTimerDownloadDatabase;



{ TimersOnTimerDownloadDatabase }

procedure TimersOnTimerDownloadDatabase.ExecuteTarget(Target: TObject);
begin
  if Target = frmDownloadDatabase.FTimerWx then
  begin
    frmDownloadDatabase.FTimerWx.Interval:= 1000; //:1秒
    frmDownloadDatabase.FuturePrepare_Basicdata( Target );
    frmDownloadDatabase.FutureExec_Basicdata( Target );
    if FAppHttpConntManager.ifAllIFutureComplete = true then
    begin
      frmDownloadDatabase.FTimerWx.Enabled := false;
      LogMe( '外层监听循环已经停止了...' ); //:外层监听循环在主线程
    end;
  end;
end;

  

        这种模式的设计思路,同样适用于,Html5 + ES6的后台专用线程;也适用于在“微信小程序 ”和“支付宝小程序 ”中的并发请求设计。

        有关es6后台专用线程,可参考笔者另外1个博客:

       《浏览器跨标签页通信BroadCast和ServiceWorker-连载1

        笔者在这个“验证实例”中,得到了验证:

        假设某用户有23个销售员,每个销售员,每月会分别有其“销售明细报表”和“费用明细报表”,来方便会计与其核对“销售提成报表”的数据的正确性,那么每个月,会产生23*2+1 = 47个报表,其中,“销售明细报表”的数据流相对是比较大的,明细到每单、每笔产品的销售明细;最后,还要分别自动产生这47个报表的可输出的PDF或Excel等报表,以方便微信等社交媒体分享:

        同时,由于上述结果的输出,涉及到大量基础资料(更新频率相对较低的资源:比如“员工资料”、“客户资料”、“产品资料”、“上期成本资料”等),为提升客户体验,需要在应用输出前,就后台静默地被base64加密压缩下载到本地“持久化”,当这些资源需要被本地引用时,快速高效地载入内存。

       如果这些统计报表,在线,一个一个地点击下载,再进行UI加载,就太慢了,操作起来,也过于繁琐复杂,所以“并发http请求”,就显示出其强大的优势。

喜欢的,就收藏并点个赞,鼓励我继续技术的原创写作及经验分享:

浏览器Disk Cache磁盘缓存及其协商缓存、及原生App和浏览器实现缓存的差异》 

大厂后台管理passportal鉴权登录的通行做法-之腾讯云研究


 

猜你喜欢

转载自blog.csdn.net/pulledup/article/details/129880302