mirror of
https://github.com/LCTT/TranslateProject.git
synced 2024-12-26 21:30:55 +08:00
PRF:20171004 Concurrent Servers Part 2 - Threads.md
@GitFuture
This commit is contained in:
parent
f21ab48a0b
commit
31496858a7
@ -1,23 +1,21 @@
|
||||
[并发服务器:第二节 - 线程][19]
|
||||
并发服务器(二):线程
|
||||
============================================================
|
||||
|
||||
这是并发网络服务器系列的第二节。[第一节][20] 提出了服务端实现的协议,还有简单的有序服务器的代码,是这整个系列的基础。
|
||||
这是并发网络服务器系列的第二节。[第一节][20] 提出了服务端实现的协议,还有简单的顺序服务器的代码,是这整个系列的基础。
|
||||
|
||||
这一节里,我们来看看怎么用多线程来实现并发,用 C 实现一个最简单的多线程服务器,和用 Python 实现的线程池。
|
||||
|
||||
该系列的所有文章:
|
||||
|
||||
* [第一节 - 简介][8]
|
||||
|
||||
* [第二节 - 线程][9]
|
||||
|
||||
* [第三节 - 事件驱动][10]
|
||||
|
||||
### 多线程的方法设计并发服务器
|
||||
|
||||
说起第一节里的有序服务器的性能,最显而易见的,是在服务器处理客户端连接时,计算机的很多资源都被浪费掉了。尽管假定客户端快速发送完消息,不做任何等待,仍然需要考虑网络通信的开销;网络要比现在的 CPU 慢上百万倍还不止,因此 CPU 运行服务器时会等待接收套接字的流量,而大量的时间都花在完全不必要的等待中,。
|
||||
说起第一节里的顺序服务器的性能,最显而易见的,是在服务器处理客户端连接时,计算机的很多资源都被浪费掉了。尽管假定客户端快速发送完消息,不做任何等待,仍然需要考虑网络通信的开销;网络要比现在的 CPU 慢上百万倍还不止,因此 CPU 运行服务器时会等待接收套接字的流量,而大量的时间都花在完全不必要的等待中。
|
||||
|
||||
这里是一份示意图,表明有序时客户端的运行过程:
|
||||
这里是一份示意图,表明顺序时客户端的运行过程:
|
||||
|
||||
![顺序客户端处理流程](https://eli.thegreenplace.net/images/2017/sequential-flow.png)
|
||||
|
||||
@ -127,7 +125,7 @@ INFO:2017-09-20 06:31:58,837:conn0 disconnecting
|
||||
|
||||
尽管在现代操作系统中就资源利用率方面来看,线程相当的高效,但前一节中讲到的方法在高负载时却会出现纰漏。
|
||||
|
||||
想象一下这样的情景:很多客户端同时进行连接,某些回话持续的时间长。这意味着某个时刻服务器上有很多活跃的线程。太多的线程会消耗掉大量的内存和 CPU 资源,仅仅是用于上下文切换 [ [1][13] ]。一个可行的方法是将其视为安全问题:因为这样的设计容易让服务器成为 [Dos 攻击][14] 的目标 —— 上百万个客户端同时连接,并且客户端都处于闲置状态,这样耗尽了所有资源就就可能让服务器宕机。
|
||||
想象一下这样的情景:很多客户端同时进行连接,某些会话持续的时间长。这意味着某个时刻服务器上有很多活跃的线程。太多的线程会消耗掉大量的内存和 CPU 资源,而仅仅是用于上下文切换^注1 。另外其也可视为安全问题:因为这样的设计容易让服务器成为 [DoS 攻击][14] 的目标 —— 上百万个客户端同时连接,并且客户端都处于闲置状态,这样耗尽了所有资源就可能让服务器宕机。
|
||||
|
||||
当服务器要与每个客户端通信,CPU 进行大量计算时,就会出现更严重的问题。这种情况下,容易想到的方法是减少服务器的响应能力 —— 只有其中一些客户端能得到服务器的响应。
|
||||
|
||||
@ -143,13 +141,13 @@ INFO:2017-09-20 06:31:58,837:conn0 disconnecting
|
||||
|
||||
非常明显,线程池的定义就是一种按比例限制的机制。我们可以提前设定服务器所能拥有的线程数。那么这就是并发连接的最多的客户端数 —— 其它的客户端就要等到线程空闲。如果我们的池中有 8 个线程,那么 8 就是服务器可以处理的最多的客户端并发连接数,哪怕上千个客户端想要同时连接。
|
||||
|
||||
那么怎么确定池中需要有多少个线程呢?通过对问题范畴进行细致的分析,评估,实验以及根据我们拥有的硬件配置。如果是单核的云服务器,答案只有一个;如果是 100 核心的多套接字的服务器,那么答案就有很多种。也可以在运行时根据负载动态选择池的大小 —— 我会在这个系列之后的文章中谈到这个东西。
|
||||
那么怎么确定池中需要有多少个线程呢?通过对问题范畴进行细致的分析、评估、实验以及根据我们拥有的硬件配置。如果是单核的云服务器,答案只有一个;如果是 100 核心的多套接字的服务器,那么答案就有很多种。也可以在运行时根据负载动态选择池的大小 —— 我会在这个系列之后的文章中谈到这个东西。
|
||||
|
||||
使用线程池的服务器在高负载情况下表现出 _性能退化_ —— 客户端能够以稳定的速率进行连接,可能会比其它时刻得到响应的用时稍微久一点;也就是说,无论多少个客户端同时进行连接,服务器总能保持响应,尽最大能力响应等待的客户端。与之相反,每个客户端一个线程的服务器,会接收多个客户端的连接直到过载,这时它更容易崩溃或者因为要处理_所有_客户端而变得缓慢,因为资源都被耗尽了(比如虚拟内存的占用)。
|
||||
|
||||
### 在服务器上使用线程池
|
||||
|
||||
为了 [改变服务器的实现][16],我用了 Python,在 Python 的标准库中带有一个已经实现好的稳定的线程池。(`concurrent.futures` 模块里的 `ThreadPoolExecutor`) [ [2][17] ]。
|
||||
为了[改变服务器的实现][16],我用了 Python,在 Python 的标准库中带有一个已经实现好的稳定的线程池。(`concurrent.futures` 模块里的 `ThreadPoolExecutor`) ^注2 。
|
||||
|
||||
服务器创建一个线程池,然后进入循环,监听套接字接收客户端的连接。用 `submit` 把每一个连接的客户端分配到池中:
|
||||
|
||||
@ -245,25 +243,23 @@ INFO:2017-09-22 05:58:57,238:conn2 disconnecting
|
||||
|
||||
回顾之前讨论的服务器行为:
|
||||
|
||||
1. 在有序服务器中,所有的连接都是串行的。一个连接结束后,下一个连接才能开始。
|
||||
|
||||
1. 在顺序服务器中,所有的连接都是串行的。一个连接结束后,下一个连接才能开始。
|
||||
2. 前面讲到的每个客户端一个线程的服务器中,所有连接都被同时接受并得到服务。
|
||||
|
||||
这里可以看到一种可能的情况:两个连接同时得到服务,只有其中一个结束连接后第三个才能连接上。这就是把线程池大小设置成 2 的结果。真实用例我们会把线程池设置的更大些,取决于机器和实际的协议。线程池的缓冲机制就能很好理解了 —— 我 [几个月前][18] 更详细的介绍过这种机制,关于 Clojure 的 `core.async` 模块。
|
||||
这里可以看到一种可能的情况:两个连接同时得到服务,只有其中一个结束连接后第三个才能连接上。这就是把线程池大小设置成 2 的结果。真实用例中我们会把线程池设置的更大些,取决于机器和实际的协议。线程池的缓冲机制就能很好理解了 —— 我 [几个月前][18] 更详细的介绍过这种机制,关于 Clojure 的 `core.async` 模块。
|
||||
|
||||
### Summary and next steps总结与展望
|
||||
### 总结与展望
|
||||
|
||||
这篇文章讨论了在服务器中,用多线程作并发的方法。每个客户端一个线程的方法最早提出来,但是实际上却不常用,因为它并不安全。
|
||||
|
||||
线程池就常见多了,最受欢迎的几个编程语言有良好的实现(某些编程语言,像 Python,就是标准库中的实现)。这里说的使用线程池的服务器,不会受到每个客户端一个线程的弊端。
|
||||
线程池就常见多了,最受欢迎的几个编程语言有良好的实现(某些编程语言,像 Python,就是在标准库中实现)。这里说的使用线程池的服务器,不会受到每个客户端一个线程的弊端。
|
||||
|
||||
然而,线程不是处理多个客户端并行访问的唯一方法。下一节中我们会看看其它的解决方案,可以使用_异步处理_,或者_事件驱动_的编程。
|
||||
|
||||
* * *
|
||||
|
||||
[ [1][1] ] 老实说,现代 Linux 内核可以承受足够多的并发线程 —— 只要这些线程主要在 I/O 上被阻塞。[这里有个示例程序][2],它产生可配置数量的线程,线程在循环体中是休眠的,每 50 ms 唤醒一次。我在 4 核的 Linux 机器上可以轻松的产生 10000 个线程;哪怕这些线程大多数时间都在睡眠,它们仍然消耗一到两个核心,以便实现上下文切换。而且,它们占用了 80 GB 的虚拟内存(Linux 上每个线程的栈大小默认是 8MB)。实际使用中,线程会使用内存并且不会在循环体中休眠,因此它可以非常快的占用完一个机器的内存。
|
||||
|
||||
[ [2][3] ] 自己动手实现一个线程池是个有意思的练习,但我现在还不想做。我曾写过用来练手的 [针对特殊任务的线程池][4]。是用 Python 写的;用 C 重写的话有些难度,但对于经验丰富的程序员,几个小时就够了。
|
||||
- 注1:老实说,现代 Linux 内核可以承受足够多的并发线程 —— 只要这些线程主要在 I/O 上被阻塞。[这里有个示例程序][2],它产生可配置数量的线程,线程在循环体中是休眠的,每 50 ms 唤醒一次。我在 4 核的 Linux 机器上可以轻松的产生 10000 个线程;哪怕这些线程大多数时间都在睡眠,它们仍然消耗一到两个核心,以便实现上下文切换。而且,它们占用了 80 GB 的虚拟内存(Linux 上每个线程的栈大小默认是 8MB)。实际使用中,线程会使用内存并且不会在循环体中休眠,因此它可以非常快的占用完一个机器的内存。
|
||||
- 注2:自己动手实现一个线程池是个有意思的练习,但我现在还不想做。我曾写过用来练手的 [针对特殊任务的线程池][4]。是用 Python 写的;用 C 重写的话有些难度,但对于经验丰富的程序员,几个小时就够了。
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@ -271,7 +267,7 @@ via: https://eli.thegreenplace.net/2017/concurrent-servers-part-2-threads/
|
||||
|
||||
作者:[Eli Bendersky][a]
|
||||
译者:[GitFuture](https://github.com/GitFuture)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
校对:[wxy](https://github.com/wxy)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
@ -283,7 +279,7 @@ via: https://eli.thegreenplace.net/2017/concurrent-servers-part-2-threads/
|
||||
[5]:https://eli.thegreenplace.net/tag/concurrency
|
||||
[6]:https://eli.thegreenplace.net/tag/c-c
|
||||
[7]:https://eli.thegreenplace.net/tag/python
|
||||
[8]:http://eli.thegreenplace.net/2017/concurrent-servers-part-1-introduction/
|
||||
[8]:https://linux.cn/article-8993-1.html
|
||||
[9]:http://eli.thegreenplace.net/2017/concurrent-servers-part-2-threads/
|
||||
[10]:http://eli.thegreenplace.net/2017/concurrent-servers-part-3-event-driven/
|
||||
[11]:https://github.com/eliben/code-for-blog/blob/master/2017/async-socket-server/threaded-server.c
|
||||
@ -295,4 +291,4 @@ via: https://eli.thegreenplace.net/2017/concurrent-servers-part-2-threads/
|
||||
[17]:https://eli.thegreenplace.net/2017/concurrent-servers-part-2-threads/#id4
|
||||
[18]:http://eli.thegreenplace.net/2017/clojure-concurrency-and-blocking-with-coreasync/
|
||||
[19]:https://eli.thegreenplace.net/2017/concurrent-servers-part-2-threads/
|
||||
[20]:http://eli.thegreenplace.net/2017/concurrent-servers-part-1-introduction/
|
||||
[20]:https://linux.cn/article-8993-1.html
|
||||
|
Loading…
Reference in New Issue
Block a user