【译】提升nginx9倍性能的线程池

在nginx的官网看到一篇介绍nginx原理的文章,这篇文章比较老了是15年发布的,国内有人翻译过可是有些小瑕疵,这里更正出来发布在我我的的文章里供你们参考,这篇文章详细的介绍了nginx线程池的原理以及设计思路,在最后经过详细的实验数据来告诉咱们经过线程池提高的性能以及分析了应该使用线程池的场景。在往后的其余领域依然颇有借鉴意义。html

点我看源文linux

你们都知道NGINX使用异步以及事件驱动的方式来处理请求,这意味着,咱们不会为每一个请求建立另外一个专用的进程或线程(好比像那些使用了传统架构的服务器)。而是选择一个工做进程来处理多个链接请求。为了实现这样的特性,NGINX使用非阻塞模式下的socket以及选择了更有效率的系统调用好比epollkqueue
满负载的进程数量不多(一般是每一个cpu核心只占一个)并且是恒定的,这样消耗了更少的内存以及cpu时间片没有被浪费在任务切换上。这个方法的优势能够经过nginx这个例子来反映出来。它能够很是好的并发处理上百万的请求规模而且处理的效果还不错。nginx

clipboard.png
每一个进程消耗额外的内存,每次进程之间的切换会消耗CPU周期以及产生cpu缓存垃圾数据库

可是异步,事件驱动这类模型一样存在问题。或者我更喜欢把这样的问题称做敌人。敌人的名字叫作:阻塞。不幸的是,许多第三方模块都使用了阻塞式的调用,并且用户(有时候甚至模块的开发者)没有意识到这么作的弊端。阻塞式的操做会毁掉NGINX的性能因此不管如何必定要被阻止。后端

可是在如今的官方版本的NGINX源代码中也不可能在任何状况下避免阻塞,为了解决这个问题新的“线程池”特性被引入到NGINX version 1.7.11以及NGINX PLUS Release 7当中来.它是什么以及它如何使用这个咱们稍后讨论,如今咱们来面对咱们的敌人了。缓存

编辑注-想对NGINX PLUS R7有个大概了解能够在咱们的博客看到更多
想看NGINX PLUS R7其余特性的具体分析,能够看下边列出来的博客:服务器

问题

首先,为了更好的了解NGINX咱们会用几句话解释一下它是如何工做的。
大致来讲,NGINX是一个事件处理器,一个从内核接收目前的链接信息而后发送接下来作的什么命令给操做系统的控制器。实际上NGINX作的脏活累活是经过协调操做系统来作的,本质上是操做系统在作周期性的读或者写。因此对NGINX来讲反应的快速及时是很重要的。

clipboard.png
工做进程监听以及处理从内核传过来的事件

这些事件可能会超时,通知socket读就绪或者写就绪,或者通知一个错误的产生。NGINX接收到一串事件接着一个一个的处理它们。因此全部的操做能够在一个线程的一个队列的一次简单循环当中完成。NGINX从队列当中弹出一个事件而后经过好比读写socket来作后续处理。在大多数状况下,这个操做会很快(也许这个操做只须要不多的cpu时间片去从内存当中copy一些数据)而且NGINX能够用很短的时间在这个队列当中处理完全部的事件。

clipboard.png
全部的操做都在一个线程的简单循环当中作完了。
可是若是一个长时间而且重量级的操做到来会发生啥呢?答案显而易见,整个事件处理的循环都会被这个操做所阻塞直到这个操做完成。

所以,所谓“阻塞操做”是指任何致使事件处理循环显著中止一段时间的操做。操做会由于各类各样的缘由而被阻塞。好比说,NGINX可能忙于处理冗长的CPU密集型处理,或者可能须要等待访问资源(例如硬盘驱动器,或一个库函数以同步方式从数据库获取响应,等等等等)。关键的问题在于,处理这样的事情,工做线程不能作其余别的事情,即便其余的系统资源能够获取到并且队列当中的其余一些事件会用到这些资源。

想象一下商店销售员面前有长长的一长队人。 队列中的第一我的须要一个不在商店可是在仓库里的东西。 销售人员跑到仓库去提货。 如今,整个队列必须等待几个小时才能进行交付,排队当中的每一个人都不满意。 想一想若是是你你会如何反应? 队员中的每一个人的等待时间都增长了这几个小时,但他们打算买的东西有可能就在店里。

clipboard.png
队列里的每一个人都必须由于第一个的订单而等待。

相同的状况发生在NGINX当中,想一想当它想要读一个没有缓存在内存中的文件而不得不去访问硬盘的时候。硬盘驱动器很慢(特别是机械硬盘),而等待队列中的其余请求可能不须要访问驱动器,因此它们也是被迫等待的。 所以,延迟增长,系统资源未获得充分利用。

clipboard.png
只有一个阻塞会大幅延迟全部的接下来全部的操做

一些操做系统提供用于读取和发送文件的异步接口,NGINX可使用此接口(请参阅aio指令)。 这里有个好例子就是FreeBSD。坑爹的是,Linux可能不如左边这位那么友好。 虽然Linux提供了一种用于读取文件的异步接口,但它有一些显著的缺点。 其中一个是文件访问和缓冲区的对齐要求,固然NGINX能够把这个问题处理得很好。 但第二个问题更糟糕,异步接口须要在文件描述符上设置O_DIRECT标志,这意味着对文件的任何访问将绕过内存中的缓存并增长硬盘上的负载。这无形中干掉了不少本来可使用这个调用的场景。

为了特别解决这个问题,NGINX 1.7.11和NGINX Plus Release 7中引入了线程池。

如今咱们来看看什么线程池是关于它们以及它们的工做原理。

线程池

让咱们回到上个问题,倒霉的销售助理从遥远的仓库配货这个用例。 此次他变得更聪明(也许是被愤怒的客户群殴后变得更聪明了),他雇佣了送货服务。 如今,当有人须要遥远的仓库里的一些东西的时候,他不会亲自去仓库而只不过下了一个订单到送货服务,他们会处理订单,而咱们的销售助理会继续为其余客户服务。 所以,只有那些货物不在商店的客户正在等待交货,而售货员能够立刻继续为其余客户提供服务。

图片描述
将订单传递给运送服务从而解除阻塞队列

在NGINX方面,线程池正在执行运送服务的功能。 它由一个任务队列和多个处理队列的线程组成。 当一个工做进程须要作一个潜在的长时间操做时,它不会本身处理这个操做,而是将一个任务放在线程池的队列中,任何空闲的线程均可以从中进行处理。

clipboard.png
工做进程将阻塞操做装载到线程池

看来咱们还有一个队列。是的,可是在这种状况下,队列受到特定资源的限制。咱们从磁盘读取资源速度永远比磁盘生成数据要慢。可是如今至少磁盘操做不会延迟其余事件的处理,只有须要访问文件的请求正在等待。

一般将“从磁盘读取”操做用做阻塞操做的最多见示例,但实际上NGINX中的线程池实现适用于任何不适合在主工做循环中处理的任务。

目前,提交到线程池仅用于三个基本操做:大多数操做系统上的read()系统调用,Linux上的sendfile()和Linux上的在编写一些临时文件好比缓存时使用到的aio_write()。咱们将继续测试和评估,若是有明显的好处,咱们可能会在将来的版本中将其余操做也提交到线程池。

编辑注: 在NGINX 1.9.13和NGINX Plus R9中添加了对aio_write()系统调用的支持。

评估基准

如今到了理论通往实践的时候了。 为了演示使用线程池的效果,咱们将执行一个合成基准,模拟阻塞和非阻塞操做的最糟糕组合。

它须要一个确保不适合内存贮存的数据集。 在具备48 GB内存的机器上,咱们已经生成了256 GB的随机4M分割数据,而后配置了NGINX version 1.9.0来为其提供服务。

配置很是简单:

worker_processes 16;

events {
    accept_mutex off;
}

http {
    include mime.types;
    default_type application/octet-stream;

    access_log off;
    sendfile on;
    sendfile_max_chunk 512k;

    server {
        listen 8000;

        location / {
            root /storage;
        }
    }
}

能够看到的是,为了得到更好的性能,一些调优已经提早作完:logging和accept_mutex被禁用,sendfile被启用,而且sendfile_max_chunk被设置。 最后一个指令能够减小阻止sendfile()调用所花费的最大时间,由于NGINX不会一次尝试发送整个文件,而是分割成512 KB的数据块来执行相应操做。

该机器有两块Intel Xeon E5645(12核24线程)处理器和10 Gbps网络接口。 磁盘子系统由安装在RAID10阵列中的四个西数WD1003FBYX硬盘驱动器表示。 操做系统是Ubuntu Server 14.04.1 LTS。

clipboard.png
相应基准下负载生成和NGINX的配置。

客户由两台相同的规格的机器组成。其中一个机器上,wrk使用Lua脚本建立负载。脚本以200的并发链接从服务器以随机顺序请求文件,和每一个请求均可能会致使缓存缺失从而致使从磁盘读取产生的阻塞。咱们就叫它“加载随机载荷”。

第二客户端机器咱们将运行另外一个副本的wrk,可是这个脚本咱们使用50的并发链接来请求相同的文件。由于这个文件被常常访问的,它将保持在内存中。在正常状况下,NGINX很快的处理这些请求,可是工做线程若是被其余的请求阻塞性能将会降低。因此咱们暂且叫它“加载恒定负载”。

性能将由服务器上ifstat监测的吞吐率(throughput)和从第二台客户端获取的wrk结果来度量。

如今,第一次没有线程池给了咱们不是那么让人赛艇的结果:

% ifstat -bi eth2
eth2
Kbps in  Kbps out
5531.24  1.03e+06
4855.23  812922.7
5994.66  1.07e+06
5476.27  981529.3
6353.62  1.12e+06
5166.17  892770.3
5522.81  978540.8
6208.10  985466.7
6370.79  1.12e+06
6123.33  1.07e+06

如你所见,上述的配置能够产生一共1G的流量,从top命令上咱们能够看到全部的工做线程在阻塞io上花费了大量的时间(下图D状态):

top - 10:40:47 up 11 days,  1:32,  1 user,  load average: 49.61, 45.77 62.89
Tasks: 375 total,  2 running, 373 sleeping,  0 stopped,  0 zombie
%Cpu(s):  0.0 us,  0.3 sy,  0.0 ni, 67.7 id, 31.9 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:  49453440 total, 49149308 used,   304132 free,    98780 buffers
KiB Swap: 10474236 total,    20124 used, 10454112 free, 46903412 cached Mem

  PID USER     PR  NI    VIRT    RES     SHR S  %CPU %MEM    TIME+ COMMAND
 4639 vbart    20   0   47180  28152     496 D   0.7  0.1  0:00.17 nginx
 4632 vbart    20   0   47180  28196     536 D   0.3  0.1  0:00.11 nginx
 4633 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.11 nginx
 4635 vbart    20   0   47180  28136     480 D   0.3  0.1  0:00.12 nginx
 4636 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.14 nginx
 4637 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.10 nginx
 4638 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.12 nginx
 4640 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.13 nginx
 4641 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.13 nginx
 4642 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.11 nginx
 4643 vbart    20   0   47180  28276     536 D   0.3  0.1  0:00.29 nginx
 4644 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.11 nginx
 4645 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.17 nginx
 4646 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.12 nginx
 4647 vbart    20   0   47180  28208     532 D   0.3  0.1  0:00.17 nginx
 4631 vbart    20   0   47180    756     252 S   0.0  0.1  0:00.00 nginx
 4634 vbart    20   0   47180  28208     536 D   0.0  0.1  0:00.11 nginx<
 4648 vbart    20   0   25232   1956    1160 R   0.0  0.0  0:00.08 top
25921 vbart    20   0  121956   2232    1056 S   0.0  0.0  0:01.97 sshd
25923 vbart    20   0   40304   4160    2208 S   0.0  0.0  0:00.53 zsh

在这种状况下,吞吐率受限于磁盘子系统,而CPU在大部分时间里是空转状态的。从wrk得到的结果来看也很是低:

Running 1m test @ http://192.0.2.1:8000/1/1/1
  12 threads and 50 connections
  Thread Stats   Avg    Stdev     Max  +/- Stdev
    Latency     7.42s  5.31s   24.41s   74.73%
    Req/Sec     0.15    0.36     1.00    84.62%
  488 requests in 1.01m, 2.01GB read
Requests/sec:      8.08
Transfer/sec:     34.07MB

请记住,文件是从内存送达的!第一个客户端的200个链接建立的随机负载,使服务器端的所有的工做进程忙于从磁盘读取文件,所以产生了过大的延迟,而且没法在合适的时间内处理咱们的请求。

而后亮出线程池了。为此,咱们只需在location块中添加aio threads指令:

location / {
root /storage;
aio threads;
}

接着,执行NGINX reload从新加载配置。

而后,咱们重复上述的测试:

% ifstat -bi eth2
eth2
Kbps in  Kbps out
60915.19  9.51e+06
59978.89  9.51e+06
60122.38  9.51e+06
61179.06  9.51e+06
61798.40  9.51e+06
57072.97  9.50e+06
56072.61  9.51e+06
61279.63  9.51e+06
61243.54  9.51e+06
59632.50  9.50e+06

如今咱们的服务器产生9.5 Gbps的流量,对比以前没有线程池时的1 Gbps高下立判!

理论上还能够产生更多的流量,可是这已经达到了机器的最大网络吞吐能力,因此在此次NGINX的测试中,NGINX受限于网络接口。工做进程的大部分时间只是休眠和等待新的事件(它们在下图处于top的S状态):

top - 10:43:17 up 11 days,  1:35,  1 user,  load average: 172.71, 93.84, 77.90
Tasks: 376 total,  1 running, 375 sleeping,  0 stopped,  0 zombie
%Cpu(s):  0.2 us,  1.2 sy,  0.0 ni, 34.8 id, 61.5 wa,  0.0 hi,  2.3 si,  0.0 st
KiB Mem:  49453440 total, 49096836 used,   356604 free,    97236 buffers
KiB Swap: 10474236 total,    22860 used, 10451376 free, 46836580 cached Mem

  PID USER     PR  NI    VIRT    RES     SHR S  %CPU %MEM    TIME+ COMMAND
 4654 vbart    20   0  309708  28844     596 S   9.0  0.1  0:08.65 nginx
 4660 vbart    20   0  309748  28920     596 S   6.6  0.1  0:14.82 nginx
 4658 vbart    20   0  309452  28424     520 S   4.3  0.1  0:01.40 nginx
 4663 vbart    20   0  309452  28476     572 S   4.3  0.1  0:01.32 nginx
 4667 vbart    20   0  309584  28712     588 S   3.7  0.1  0:05.19 nginx
 4656 vbart    20   0  309452  28476     572 S   3.3  0.1  0:01.84 nginx
 4664 vbart    20   0  309452  28428     524 S   3.3  0.1  0:01.29 nginx
 4652 vbart    20   0  309452  28476     572 S   3.0  0.1  0:01.46 nginx
 4662 vbart    20   0  309552  28700     596 S   2.7  0.1  0:05.92 nginx
 4661 vbart    20   0  309464  28636     596 S   2.3  0.1  0:01.59 nginx
 4653 vbart    20   0  309452  28476     572 S   1.7  0.1  0:01.70 nginx
 4666 vbart    20   0  309452  28428     524 S   1.3  0.1  0:01.63 nginx
 4657 vbart    20   0  309584  28696     592 S   1.0  0.1  0:00.64 nginx
 4655 vbart    20   0  30958   28476     572 S   0.7  0.1  0:02.81 nginx
 4659 vbart    20   0  309452  28468     564 S   0.3  0.1  0:01.20 nginx
 4665 vbart    20   0  309452  28476     572 S   0.3  0.1  0:00.71 nginx
 5180 vbart    20   0   25232   1952    1156 R   0.0  0.0  0:00.45 top
 4651 vbart    20   0   20032    752     252 S   0.0  0.0  0:00.00 nginx
25921 vbart    20   0  121956   2176    1000 S   0.0  0.0  0:01.98 sshd
25923 vbart    20   0   40304   3840    2208 S   0.0  0.0  0:00.54 zsh

如今仍然有充足的CPU资源能够利用

下边是wrk的结果:

Running 1m test @ http://192.0.2.1:8000/1/1/1
  12 threads and 50 connections
  Thread Stats   Avg      Stdev     Max  +/- Stdev
    Latency   226.32ms  392.76ms   1.72s   93.48%
    Req/Sec    20.02     10.84    59.00    65.91%
  15045 requests in 1.00m, 58.86GB read
Requests/sec:    250.57
Transfer/sec:      0.98GB

服务器处理4MB文件的平均时间从7.42秒降到226.32毫秒(减小了33倍),每秒请求处理数提高了31倍(250 vs 8)!

对此,咱们的解释是请求再也不由于工做进程被阻塞在读文件而滞留在事件队列中等待处理,它们能够被空闲的线程(线程池当中的)处理掉。只要磁盘子系统能撑住第一个客户端上的随机负载,NGINX可使用剩余的CPU资源和网络容量,从内存中读取,以服务于上述的第二个客户端的请求。

没有什么灵丹妙药

在抛出咱们对阻塞操做的担心并给出一些使人振奋的结果后,可能大部分人已经打算在你的服务器上配置线程池了。可是先别着急。

实际上很幸运大多数的读或者写文件操做都不会和硬盘打交道。若是咱们有足够的内存来存储数据集,那么操做系统会聪明地在被称做“页面缓存”的地方缓存那些频繁使用的文件。

“页面缓存”的效果很好,可让NGINX在几乎全部常见的用例中展现优异的性能。从页面缓存中读取比较快,没有人会说这种操做是“阻塞”。另外一方面,装载任务到线程池是有必定开销的。

所以,若是你的机器有合理的大小的内存而且待处理的数据集不是很大的话,那么无需使用线程池,NGINX已经工做在最优化的方式下。

装载读操做到线程池是一种适用于很是特殊任务的技巧。只有当常常请求的内容的大小不适合操做系统的虚拟机缓存时,这种技术才是最有用的。至于可能适用的场景,好比,基于NGINX的高负载流媒体服务器。这正是咱们已经上文模拟的基准测试的场景。

咱们若是能够改进装载读操做到线程池,将会很是有意义。咱们只须要知道所需的文件数据是否在内存中,只有不在内存中的时候读操做才应该装载到线程池的某个单独的线程中。

再回到售货员的场景中,这回售货员不知道要买的商品是否在店里,他必需要么老是将全部的订单提交给运货服务,要么老是亲自处理它们。

是的,问题的本质就是操做系统没有这样的特性。2010年人们第一次试图把这个功能做为fincore()系统调用加入到Linux当中,可是没有成功。后来还有一些是使用RWF_NONBLOCK标记做为preadv2()系统调用来实现这一功能的尝试(详情见LWN.net上的非阻塞缓冲文件读取操做异步缓冲读操做)。但全部这些补丁的命运目前还不明朗。悲催的是,这些补丁尚没有被内核接受的主要缘由你能够看这里:(bikeshedding)。

译者著:我以为没加入内核彻底就是开发组里面一派人有相似“要啥自行车”这样的想法.....

另外一方面,FreeBSD的用户彻底没必要担忧。FreeBSD已经具有足够好的异步读取文件接口,咱们应该用它而不是线程池。

配置线程池

因此若是你确信在你的用例中使用线程池会带来好处,那么如今就是时候深刻了解线程池的配置了。

线程池的配置很是简单、灵活。首先,获取NGINX 1.7.11或更高版本的源代码,使用--with-threads配置参数编译。在最简单的场景中,配置也看起来很简单。全部你须要的全部事就是在合适的状况下把aio线程的指令include进来:

# in the 'http', 'server', or 'location' context
aio threads;

这是线程池的最简配置。实际上边的配置是下边的精简版:

# in the 'main' context
thread_pool default threads=32 max_queue=65536;

# in the 'http', 'server', or 'location' context
aio threads=default;

这里定义了一个名为“default”,包含32个线程,任务队列最多支持65536个请求的线程池。若是任务队列过载,NGINX将拒绝请求并输出以下错误日志:

thread pool "NAME" queue overflow: N tasks waiting

错误输出意味着线程处理做业的速度有可能低于任务入队的速度了。你能够尝试增长队列的最大值,可是若是这无济于事,那这意味着你的系统没有能力处理这么多的请求了。

正如你已经注意到的,你可使用thread_pool指令,配置线程的数量、队列的最大长度,以及特定线程池的名称。最后要说明的是,能够配置多个相互独立的线程池,并在配置文件的不一样位置使用它们来知足不一样的用途:

# in the 'main' context
thread_pool one threads=128 max_queue=0;
thread_pool two threads=32;

http {
    server {
        location /one {
            aio threads=one;
        }

        location /two {
            aio threads=two;
        }

    }
    #...
}

若是没有指定max_queue参数的值,默认使用的值是65536。如上所示,能够设置max_queue为0。在这种状况下,线程池将使用配置中所有数量的线程来尽量地同时处理多个任务;队列中不会有等待的任务。

如今,假设咱们有一台服务器,挂了3块硬盘,咱们但愿把该服务器用做“缓存代理”,缓存后端服务器的所有响应。预期的缓存数据量远大于可用的内存。它其实是咱们我的CDN的一个缓存节点。毫无疑问,在这种状况下,最重要的事情是发挥硬盘的最大性能。

咱们的选择之一是配置一个RAID阵列。这种方法毁誉参半,如今,有了NGINX,咱们能够有另外的选择:

# We assume that each of the hard drives is mounted on one of these directories:
# /mnt/disk1, /mnt/disk2, or /mnt/disk3

# in the 'main' context
thread_pool pool_1 threads=16;
thread_pool pool_2 threads=16;
thread_pool pool_3 threads=16;

http {
    proxy_cache_path /mnt/disk1 levels=1:2 keys_zone=cache_1:256m max_size=1024G
                     use_temp_path=off;
    proxy_cache_path /mnt/disk2 levels=1:2 keys_zone=cache_2:256m max_size=1024G
                     use_temp_path=off;
    proxy_cache_path /mnt/disk3 levels=1:2 keys_zone=cache_3:256m max_size=1024G
                     use_temp_path=off;

    split_clients $request_uri $disk {
        33.3%     1;
        33.3%     2;
        *         3;
    }

    server {
        #...
        location / {
            proxy_pass http://backend;
            proxy_cache_key $request_uri;
            proxy_cache cache_$disk;
            aio threads=pool_$disk;
            sendfile on;
        }
    }
}

在这份配置中,使用了3个独立的缓存,每一个缓存专用一块硬盘,另外,3个独立的线程池也各自专用一块硬盘,proxy_cache_path指令在每一个磁盘定义了一个专用、独立的缓存

split_clients模块用于高速缓存之间的负载平衡(以及磁盘之间的结果),它彻底适合这类任务。

在 proxy_cache_path指令中设置use_temp_path=off,表示NGINX会将临时文件保存在缓存数据的同一目录中。这是为了不在更新缓存时,磁盘之间互相复制响应数据。

这些调优将发挥磁盘子系统的最优性能,由于NGINX经过单独的线程池并行且独立地与每块磁盘交互。每一个磁盘由16个独立线程提供支持,而且线程具备用于读取和发送文件的专用任务队列。

咱们相信你的客户会喜欢这种量身定制的方法。请确保你的磁盘撑得住。

这个示例很好地证实了NGINX能够为硬件专门调优的灵活性。这就像你给NGINX下了一道命令,要求机器和数据最优配合。并且,经过NGINX在用户空间中细粒度的调优,咱们能够确保软件、操做系统和硬件工做在最优模式下而且尽量有效地利用系统资源。

总结

综上所述,线程池是个好功能,它将NGINX的性能提升到新的高度而且干掉了一个众所周知的长期隐患:阻塞,尤为是当咱们真正面对大量吞吐的状况下这种优点更加明显。

可是还有更多的惊喜。正如前面所述,这种全新的接口可能容许装载任何耗时和阻塞的操做而不会形成任何性能的损失。 NGINX在大量新模块和功能方面开辟了新的天地。 许多受欢迎的库仍然没有提供异步非阻塞接口,之前这使得它们与NGINX不兼容。 咱们可能花费大量的时间和精力来开发本身的非阻塞原型库,可是这么作可能并不值得。 如今,使用线程池,咱们能够相对容易地使用这些库,而且这些模块不会对性能产生影响。

敬请期待下篇文章。

相关文章
相关标签/搜索