类单线程是这样一种并发模型,它把一个单线程系统扩展到N个单线程系统,结果是N个单线程系统并行运行。
类单线程系统不是纯粹的单线程系统,因为它包含多个线程。但是,每个线程都像单线程系统一样运行。因此称之为类单线程而不是单线程。
为什么要用单线程系统?
你可能好奇,为什么时至今日还有人会设计单线程系统。单线程系统之所以受欢迎,是因为其并发模型比多线程系统简单得多。单线程系统不与其他线程共享任何状态(对象/数据)。这使得单线程能够使用非并发数据结构,并更好地利用CPU和CPU缓存。
然而,单线程系统没有充分利用现代CPU。一个现代的CPU通常会有2,4,6,8或更多个核心。每个核心都作为一个单独的CPU运行。单线程系统只能使用其中一个核心,如下所示:
类单线程:单线程的扩展
为了充分利用CPU中的所有核心,可以扩展单线程系统来利用整个计算机。
每个CPU一个线程
类单线程系统通常在计算机的每个CPU中运行一个线程。如果一台计算机包含4个CPU,或者一个CPU有4个内核,那么正常情况下会运行类单线程系统的4个实例(4个单线程系统)。下图显示了这一原理:
无共享状态
类单线程系统看起来类似于传统的多线程系统,因为类单线程系统中有多个线程在运行。但两者有一个微妙的区别。
类单线程系统与传统多线程系统的区别在于,类单线程系统中的线程不共享状态。既不存在用于线程间并发访问的共享内存,也不存在用于线程间共享数据的并发数据结构等。这种差异可用下图说明:
摒弃了共享状态就使得每个线程都表现的像是一个单线程系统。然而,由于类单线程系统可以包含多个单线程,因此它实际上不是“单线程系统”。由于没有更好的名称,我发现将这样的系统称为类单线程系统比称之为“具有单线程设计的多线程系统”更准确。类单线程这个名字更容易说,也更容易理解。
类单线程基本上意味着数据处理停留在同一个线程内,并且类单线程系统中没有线程同时共享数据。有时这也被称为无共享状态并发,或分离状态并发。
负载分配
显然,类单线程系统需要在运行的单线程实例之间共享工作负载。如果只有一个线程分配了工作,系统实际上是单线程的。
如何在线程之间分配负载取决于系统的设计。我将在下面的章节中介绍一些内容。
单线程微服务
如果系统由多个微服务组成,则每个微服务都可以在单线程模式下运行。当你在同一台计算机上部署多个单线程微服务时,每个微服务可以在单独的CPU上运行一个线程。
微服务本质上不共享任何数据,因此对于类单线程系统来说,微服务是一个很好的用例。
分片数据服务
如果你的系统确实需要共享数据,或者至少需要一个数据库,那么你可以对数据库进行分片。分片意味着数据被分成多个数据库。数据通常被分割,以便所有相互关联的数据都位于同一个数据库中。例如,所有属于某个“所有者”实体的数据都将插入到同一个数据库中。不过,切分数据超出了本教程的范围,因此你需要自己搜索有关该主题的教程。
线程通信
类单线程系统中的线程通过消息传递进行通信。如果线程A想向线程B发送消息,线程A可以通过生成一个消息(字节序列)来实现。然后线程B可以复制并读取该消息(字节序列)。线程B通过复制消息的方式,确保它在读取消息时线程A无法修改该消息。消息一旦复制,线程A就无法访问该消息的拷贝了。
通过消息传递进行的线程通信如下所示:
只要适合你的系统,线程可以通过队列、管道、unix套接字、TCP套接字等进行通信。
更简单的并发模型
在类单线程系统中,每个运行在其自身线程中的系统都可以像单线程一样实现。这意味着,与线程共享状态相比,内部并发模型变得简单得多。你不必再担心并发数据结构以及此类数据结构可能导致的所有并发问题。
示意图
以下是单线程、多线程和类单线程系统的示意图,这样你可以更容易地了解它们之间的区别。 第一个图显示的是单线程系统。
第二个图显示了多线程系统,其中线程共享了数据。
第三个图显示了类单线程系统,其中有两个线程具有单独的数据,通过相互传递消息进行通信。
Thread Ops for Java
Thread Ops for Java是一个开源工具包,旨在帮助你更容易地实现分离状态的类单线程系统。它包含启动和停止单个线程的工具,以及使用单个线程实现某种程度的并发性。如果你对使用类单线程来设计应用程序感兴趣,那么你可能会对Thread Ops感兴趣。