在这篇文章中,我们将更深入地了解基于线程的异步编程方法。
什么是基于线程的异步编程?
基于线程的异步编程方法,允许一个线程池将任务移交给另一个线程池,即工作线程池,然后在工作线程池完成任务后得到通知处理结果。
从调用者线程的角度来看,系统现在变得 异步的因为它在单个调用路径上的所有工作都不是按顺序进行的,它先做一些事情,然后把与IO相关的任务交给一个或多个工作线程池,然后再回来继续执行,从这一点开始,它在这中间做了一些完全独立的任务。
工人线程池上的线程仍然会因为IO而被阻塞,但现在只有这个线程池的线程会被阻塞,从而限制了系统的成本。系统中不涉及IO活动的其他代码路径被释放的调用者线程扩大了规模。服务吞吐量大大增加,因为调用者线程不会坐在那里等待IO完成,它可以执行其他计算。
这就是为什么我们在应用层分配了固定数量的数据库和TCP连接池线程的原因。在一个微服务中,我们可以让所有想要调用另一个微服务的个人线程通过创建自己的TCP连接发射API,然后等待响应。然而,在我们等待的时候,系统还可以做其他事情。我们可以创建一个较小的TCP连接的工作池,并通过它输送所有的API调用,将调用者线程从这种阻塞中解放出来。通过在这个较小的线程池中复用调用,我们可以释放出大量的其他线程来做与IO无关的工作。
了解这种行为的一个真实例子是商店里的结账柜台。只要不是每个人都在同一时间来结账,少量的结账柜台就能够处理大量的顾客。结账柜台上只有少数工人被挡在结账功能上,其他工人可以自由地协助其他顾客。
处理任务完成
通过将任务卸载到一个工人线程,调用者线程需要知道,当它从工人池线程收到任务的结果时,它在执行路径中的位置。
这个问题通常通过引入 回调这些方法将由工作池线程在完成给它的任务时调用。调用者线程将这些回调注册到 未来对象上注册这些回调,现在语言/框架的责任是通过向调用者线程发出中断(让它停止正在做的事情)并指示它执行相关的回调来调用它们。
执行模式看起来是这样的→线程1调用工人池,给它一个任务。工作者池中的线程2执行该任务并调用回调。线程1被打断,并切换到执行回调。
回调的执行是由语言/框架决定的。不同的语言可以以不同的方式实现这种执行模式。例如,在java/scalaExecutorService 等语言中,框架有责任在未来完成后执行回调,回调可以在调用者线程中执行,也可以从池本身的任何线程中执行。
使用基于线程的异步编程的注意事项
- 一旦你有一些并行和串行任务交接创建回调地狱,异步代码可能会有点复杂,难以理解和调试,这是所有异步编程语言的最大问题之一。
- ThreadLocal变量不再起作用。由于调用者线程将工作移交给另一个线程,并转移到其他任务,任何存储为ThreadLocal的上下文,例如:HTTP请求的请求上下文被丢失。传播它的唯一方法是在任务移交中明确地把它作为一个参数来传递。
尽管工人线程模型给了我们很大的改进,但它更侧重于隔离和控制有限的线程阻塞的问题。调用者线程只是假装解除了阻塞,但本质上它的阻塞已经被推送到其他线程上,浪费了资源。IO仍然是阻塞的,每个IO任务仍然需要线程。
我们可以使用真正的非阻塞式IO范式,又称基于事件的异步编程,就像在node js中一样,线程在IO中永远不会被阻塞,我们不需要创建和维护工作线程池。在下一篇博客中,我们将探讨这种模式。
构建可组合的网络应用
不要建立网络单体。使用 比特来创建和组成解耦的软件组件--在你喜欢的框架中,如React或Node。构建可扩展和模块化的应用程序,提供强大和愉快的开发体验。
把你的团队带到 比特云来托管并共同协作开发组件,并作为一个团队加快、扩大和规范开发。尝试用设计系统 或微前端的可组合前端,或探索用服务器端组件的可组合后端。