并发¶
本文档描述了 OkHttp 中 http/2 连接和连接池的并发注意事项。
HTTP/2 连接¶
HttpURLConnection API 是一个阻塞 API。你进行阻塞写操作来发送请求,进行阻塞读操作来接收响应。
阻塞 API¶
阻塞 API 很方便,因为你可以获得自上而下、无需间接操作的程序代码。网络调用就像普通方法调用一样:请求数据然后返回。如果请求失败,你会在调用处立即获得一个堆栈跟踪。
阻塞 API 可能效率低下,因为在等待网络时会使线程处于空闲状态。线程很昂贵,因为它既有内存开销,又有上下文切换开销。
分帧协议¶
像 http/2 这样的分帧协议不适用于阻塞 API。每个应用层线程都想为特定的流进行阻塞 I/O,但这些流在同一个 socket 上是多路复用的。你不能直接与 socket 通信,你需要与你共享 socket 的其他应用层线程进行协作。
分帧规则使得在单个阻塞线程上正确实现 http/2 变得不切实际。流控特性在读和写之间引入了反馈,要求写确认读,读限制写。
在 OkHttp 中,我们在分帧协议之上暴露了一个阻塞 API。本文档解释了实现此功能的代码和策略。
线程¶
应用的调用线程¶
应用层必须阻塞写入 I/O。在我们将其字节推送到 socket 上之前,我们不能从写入中返回。否则,如果写入失败,我们将无法将其 IOException 传递给应用程序。我们会告诉应用层写入成功,但它并没有!
应用层也可以进行阻塞读取。如果应用请求读取且没有可用数据,我们需要 hold 住该线程,直到字节到达、流关闭或超时。如果我们收到了字节但没有人请求,我们会将其缓冲起来。直到应用消费了字节,我们才将其视为已传递给流控。
考虑一个应用通过 http/2 流传输视频。也许用户暂停了视频,应用停止读取该流的字节。缓冲区将填满,流控阻止服务器在该流上发送更多数据。当用户取消暂停视频时,缓冲区清空,读取被确认,服务器继续流传输数据。
共享读取线程¶
我们不能依赖应用线程来从 socket 读取数据。应用线程是短暂的:有时它们在读写,有时它们在做应用层的事情。但 socket 是永久的,它需要持续关注:我们分发所有传入的帧,以便应用层需要时连接即可使用。
因此,我们为每个 socket 分配了一个专门的线程,它只读取帧并分发它们。
读取线程绝不能运行应用层代码。否则,一个慢速流可能会阻塞整个连接。
类似地,读取线程绝不能在写入时阻塞,因为这会使连接死锁。考虑一个客户端和服务器都违反此规则的情况。如果运气不好,它们可能会填满 TCP 缓冲区(导致写入阻塞),然后使用它们的读取线程写入一个帧。两端都没有人在读取,缓冲区也永远不会被清空。
稍后执行的线程池¶
有时需要执行某个操作,例如调用应用层或响应 ping,而发现该操作的线程不是应该执行工作的线程。我们将一个 runnable 加入此执行器队列,然后由执行器的一个线程处理它。
锁¶
我们有 3 个不同的东西进行同步。
Http2Connection¶
这个锁保护每个连接的内部状态。在执行阻塞操作时绝不持有此锁。这意味着我们获取锁,读取或写入一些字段,然后释放锁。没有 I/O 和应用层回调。
Http2Stream¶
这个锁保护每个流的内部状态。如上所述,在执行阻塞操作时绝不持有此锁。当我们为了阻塞读取而需要 holding 住应用线程时,我们使用此锁上的 wait/notify。这之所以有效,是因为在 wait()
等待时锁会被释放。
Http2Writer¶
socket 写入由 Http2Writer 保护。一次只能有一个流进行写入,以免消息交错。写入要么由应用层线程执行,要么由稍后执行的线程池执行。
持有多个锁¶
在持有 Http2Writer 锁的同时,允许获取 Http2Connection 锁。反之则不行。因为获取 Http2Writer 锁可能会阻塞。
这对于创建新流时的簿记是必需的。正确的帧处理要求流 ID 在 socket 上是顺序的,因此我们需要将 ID 分配与发送 SYN_STREAM
帧捆绑在一起。
连接池¶
任何 HTTP 客户端的一项主要职责是有效地管理网络连接。创建和建立新连接会带来相当大的开销和额外的延迟。OkHttp 将尽一切努力重用现有连接,以避免这种开销和额外的延迟。
每个 OkHttpClient 都使用一个连接池。它的作用是维护所有打开连接的引用。当启动一个 HTTP 请求时,OkHttp 会尝试从池中重用现有连接。如果没有现有连接,则会创建一个新连接并放入连接池。对于 HTTP/2,连接可以立即重用。对于 HTTP/1,必须完成请求后才能重用。
由于 HTTP 请求经常并行发生,连接池必须是线程安全的。
这些是涉及建立、共享和终止连接的主要类
-
RealConnectionPool 管理 HTTP 和 HTTP/2 连接的重用,以减少延迟。每个 OkHttpClient 都有一个,并且其生命周期与 OkHttpClient 的生命周期相同。
-
RealConnection 是 HTTP/1 或 HTTP/2 连接的 socket 和流。它们是按需创建的,以满足 HTTP 请求。它们可以被重用于许多 HTTP 请求/响应交换。它们的生命周期通常比连接池短。
-
Exchange 携带一个单一的 HTTP 请求/响应对。
-
ExchangeFinder 选择哪个连接携带每个交换。在可能的情况下,它会为一次单一调用中的所有交换使用相同的连接。它优先重用池中的连接而不是建立新连接。
每个连接的锁¶
每个连接都有自己的锁。池中的连接都放在一个 ConcurrentLinkedQueue
中。由于数据竞争,此队列的迭代器可能会返回已被移除的连接。调用者必须在使用池中的连接之前检查连接的 noNewExchanges
属性。
在进行 I/O 操作(即使是关闭 socket)时绝不持有连接锁,以防止争用。
使用每个连接一个锁是为了最大化并发性。