分布式系统中节点上的进程线程纤程概念及其之间关系你懂吗?

Posted king哥Java架构

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式系统中节点上的进程线程纤程概念及其之间关系你懂吗?相关的知识,希望对你有一定的参考价值。

节点

分布式系统中,程序往往会部署到不同的节点中。不同的节点之间需要通过网络来进行通信。每个节点的独立运算的结果最终汇集以支撑起分布式系统的庞大运算量。在实际的项目中,一个节点往往是一个操作系统上的进程。

本章介绍了节点上的进程、线程、纤程的概念及其之间的关系。同时也介绍了网络通信常见的异常场景。

什么是线程

在早期的计算机操作系统中,能拥有资源和独立运行的基本单位是进程。然而随着计算机技术的发展,进程出现了很多弊端:一是由于进程是资源拥有者,创建、撤销与切换存在较大的时空开销,因此需要引入轻量型进程;二是对称多处理机(Symmetric Multi-Processor,SMP)的出现,它可以满足多个运行单位,而多个进程并行开销过大。

因此在20世纪80年代,出现了能独立运行的基本单位——线程(Thread)。

线程是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。线程之间的相互制约,致使线程在运行中呈现间断性。

典型的线程拥有3种基本状态。

  • 就绪。

  • 阻塞。

  • 执行。

线程的状态如下图所示。

分布式系统中节点上的进程、线程、纤程概念及其之间关系你懂吗?图2-1 线程的状态

就绪状态是指线程具备运行的所有条件,逻辑上可以执行,在等待处理机;执行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。线程是程序中一个单一的顺序控制流程,是进程内一个相对独立的、可调度的执行单元。

在单个程序中同时运行多个线程完成不同的工作,称为多线程。多数情况下,多线程能提升程序的性能。

进程和线程

进程和线程是并发编程的两个基本的执行单元。在大多数编程语言中,并发编程主要涉及线程。

一个计算机系统通常有许多活动的进程和线程。在给定的时间内,每个处理器中只能有一个线程得到真正的运行。对于单核处理器来说,处理时间是通过时间切片在进程和线程之间进行共享的。

进程有一个独立的执行环境。进程通常有一个完整的、私有的基本运行时资源。特别是每个进程都有自己的内存空间。操作系统的进程表(Process Table)存储了CPU寄存器值、内存映像、打开的文件、统计信息、特权信息等。进程一般定义为执行中的程序,也就是当前操作系统的某个虚拟处理器上运行的一个程序。多个进程并发共享同一个CPU以及其他硬件资源,并且这是透明的,操作系统支持进程之间的隔离。

这种并发透明性需要付出较高的代价。

进程往往被视为等同于程序或应用程序。然而,用户看到的一个单独的应用程序可能实际上是一组合作的进程。大多数操作系统都支持进程间通信(Inter Process Communication,IPC),如管道和Socket。IPC不仅用于同个系统的进程之间的通信,也可以用在不同系统的进程之间进行通信。

线程有时被称为轻量级进程(Light Weight Process,LWP)。进程和线程都提供了一个执行环境,但创建一个新的线程比创建一个新的进程需要更少的资源。线程系统一般只维护用来让多个线程共享CPU所必需的最少量信息,特别是线程上下文(Thread Context)中一般只包含CPU上下文以及某些其他线程管理信息,通常忽略那些对于多线程管理不是完全必要的信息。这样单个进程中防止数据遭到某些线程不合法的访问的任务就完全落在了应用程序开发人员的肩上。线程不像进程那样彼此隔离以及受到操作系统的自动保护,所以在多线程程序开发过程中需要开发人员做更多的努力。

线程存在于进程中,每个进程都至少有一个线程。线程共享进程的资源,包括内存和打开的文件。这使得工作变得高效,但也存在一个潜在的问题——通信。关于通信的内容,会在以后的文章中讲述。

如果你觉得自己学习效率低,缺乏正确的指导,可以加入资源丰富,学习氛围浓厚的技术圈一起学习交流吧!
[Java架构群]
群内有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的JAVA交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。

现在多核处理器或多进程的计算机系统越来越流行,这大大增强了系统的进程和线程的并发执行能力。但即便在没有多核处理器或多进程的系统中,并发仍然是可能的。关于并发的内容,会在第4章讲述。

线程和纤程

为了提高并发量,某些编程语言中提供了“纤程”(Fiber)的概念,比如Golang的goroutine,Erlang风格的actor。Java虽然没有定义纤程,但仍有一些第三方库可供选择,比如Quasar。纤程可以理解为是比线程更加细颗粒度的并发单元。

由于纤程是以用户代码方式来实现的,并不受操作系统内核管理,所以内核并不知道纤程,也就无法对纤程实现调度。纤程是根据用户定义的算法来调度的,因此,就内核而言,纤程采用了非抢占式调度方式,而线程是抢占式调度的。

一个线程可以包含一个或多个纤程。线程每次执行哪一个纤程的代码,是由用户来决定的。所以,对于开发人员来说,使用纤程可以获得更大的并发量,但同时也要面临实现调度纤程的复杂度。

编程语言中的线程对象

在面向对象语言开发过程中,每个线程都与Thread类的一个实例相关联。由于Java的流行,下文中的例子将用Java来实现和使用线程对象,以作为并发应用程序的基本原型。

定义和启动一个线程

Java中有两种创建Thread实例的方式。第一种是提供Runnable对象。

Runnable接口定义了一个方法run,用来包含线程要执行的代码。

HelloRunnable示例如下。

public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello from a runnable!");
}
public static void main(String[] args) {
(new Thread(new HelloRunnable())).start();
}
}

第二种创建Thread实例的方式是继承Thread。Thread类本身是实现Runnable,虽然它的run方法并无什么作用。HelloThread示例如下。

public class HelloThread extends Thread {
@Override
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String[] args) {
(new HelloThread()).start();
}
}

请注意,这两个例子都是调用start方法来启动线程。关于第一种方式,它使用Runnable对象,在实际应用中更普遍,因为Runnable对象可以继承Thread以外的类。第二种方式,在简单的应用程序中更容易使用,但受限于你的任务类必须继承Thread。本文推荐使用第一种方式,将Runnable任务从Thread对象中分离来执行任务。这样不仅更灵活,而且它适用于高级线程管理API。

Thread类还定义了大量的方法用于线程管理。

暂停线程执行

Thread.sleep可以让当前线程执行暂停一个时间段,这样处理器的时间就可以给其他线程使用。

sleep有两种重载形式:一种是指定睡眠时间为毫秒级,另外一种是指定睡眠时间为纳秒级。然而,这些睡眠时间不能保证是精确的,因为它们是由操作系统提供的,并受其限制,因而不能假设sleep的睡眠时间是精确的。此外,睡眠周期也可以通过中断来终止。

以下SleepMessages示例是使用sleep每隔4s输出一次消息。

public class SleepMessages {
public static void main(String[] args) throws InterruptedException
{
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too" };
for (int i = 0; i < importantInfo.length; i++) {
// 暂停4s
Thread.sleep(4000);
// 输出消息
System.out.println(importantInfo[i]);
}
}
}

请注意main声明抛出InterruptedException。当sleep是激活的时候,若有另一个线程中断当前线程,则sleep抛出异常。由于该应用程序还没有定义另一个线程来引起中断,所以考虑捕捉InterruptedException。

中断线程

中断是表明一个线程应该停止它正在做和将要做的事。线程通过在Thread对象调用interrupt来实现线程的中断。为了让中断机制能正常工作,被中断的线程必须支持自己的中断。

1.支持中断

如何实现线程支持自己的中断?这要看它目前正在做什么。如果线程调用方法频繁抛出InterruptedException异常,那么它只要在run方法捕获了异常之后返回即可。例如:

for (int i = 0; i < importantInfo.length; i++) {
// 暂停4s
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// 已经中断,无须返回更多信息
return;
}
// 输出消息
System.out.println(importantInfo[i]);
}

很多方法都会抛出InterruptedException,如sleep,它被设计成在收到中断时立即取消当前的操作并返回。

若线程长时间没有调用方法抛出InterruptedException,那么它必须定期调用Thread.interrupted,该方法在接收到中断后将返回true。例如:

for (int i = 0; i < inputs.length; i++) {
heavyCrunch(inputs[i]);
if (Thread.interrupted()) {
// 已经中断,无须返回更多信息
return;
}
}

在这个简单的例子中,代码简单地测试该中断,如果已接收到中断线程就退出。在更复杂的应用程序中,它可能会更有意义地抛出一个

InterruptedException。
if (Thread.interrupted()) {
throw new InterruptedException();
}

2.中断状态标志

中断机制是使用被称为中断状态的内部标志实现的。调用Thread.interrupt可以设置该标志。当一个线程通过调用静态方法Thread.interrupted来检查中断时,中断状态被清除。非静态方法isInterrupted用于线程查询另一个线程的中断状态,而不会改变中断状态标志。

按照惯例,任何方法因抛出一个InterruptedException而退出都会清除中断状态。当然,它可能因为另一个线程调用interrupt而让中断状态立即被重新设置。

等待另一个线程完成

join方法允许一个线程等待另一个线程完成。假设t是一个正在执行的Thread对象,那么执行方法t.join();会导致当前线程暂停执行,直到t线程终止。join允许程序员指定一个等待周期,与sleep一样,等待时间是依赖于操作系统的时间,同时不能假设join等待时间是精确的。

与sleep一样,join会通过InterruptedException退出来响应中断。

节点之间的通信
节点与节点之间是完全独立、相互隔离的,节点之间传递消息的唯一方式是通过网络进行通信。而网络往往是不可靠的,即一个节点可以向其他节点通过网络发送消息,但发送消息的节点无法确认消息是否被接收节点完整准确收到。因此,在分布式系统中往往需要考虑网络通信异常的问题。

常见的网络通信异常包含以下4种。

消息丢失

消息丢失是最常见的网络通信异常之一。对于常见的IP网络来说,网络层不保证数据报文的可靠传递,在发生网络拥塞、路由变动、设备异常等情况时,都可能发生发送的消息丢失的情况。由于网络消息丢失的异常存在,直接决定了分布式系统的协议必须能处理网络消息丢失的情况。

依据网络质量的不同,网络消息丢失的概率也不同,甚至可能出现在一段时间内某些节点之间的网络消息完全丢失的情况。如果某些节点的直接的网络通信正常或丢包率在合理范围内,而某些节点之间始终无法正常通信,则称这种特殊的网络异常为“网络分化”(NetworkPartition)。网络分化是一类常见的网络通信异常,尤其当分布式系统部署在多个机房之间时。

消息乱序

消息乱序是指节点发送的网络消息有一定的概率不是按照发送时的顺序依次到达接收节点。通常由于IP网络的存储转发机制、路由不确定性等问题,网络消息乱序也是一种常见的网络通信异常。这就要求设计分布式协议时,考虑使用序列号等机制处理网络消息的乱序问题,使得无效的、过期的网络消息不影响系统的正确性。

数据错误

网络上传输的数据有可能发生比特错误,从而造成数据错误。通常使用一定的校验码机制可以较为简单地检查出网络数据的错误,从而丢弃错误的数据。

不可靠的TCP

TCP为应用层提供了可靠的、面向连接的传输服务。该协议设计初衷就是在不可靠的网络之上建立可靠的传输服务。TCP的出色设计体现在以下3个方面。

  • TCP为传输的每个字节设置顺序递增的序列号,由接收节点在收到数据后按序列号重组数据并发送确认信息。当发现数据包丢失时,TCP重传丢失的数据包,从而解决了网络数据包丢失的问题和数据包乱序问题。

  • TCP为每个TCP数据段设置32位的校验码,从而检查数据错误问题。

  • TCP通过设置接收和发送窗口的机制极大地提高了传输性能,解决了网络传输的时延与吞吐问题。TCP最为复杂而巧妙的是其几十年来不断改进的拥塞控制算法,使得TCP可以动态感知底层链路的带宽且加以合理使用,并与其他TCP链接分享带宽。

TCP在通常情况下是非常可靠的协议,然而在分布式系统的协议设计中,不能认为所有网络通信基于TCP则通信就是可靠的。一方面,TCP保证了TCP栈之间的可靠传输,但无法保证两个上层应用之间的可靠通信。通常当某个应用层程序通过TCP的系统调用发送一个网络消息时,即使TCP系统调用返回成功,也仅仅意味着该消息被本机的TCP栈接受,一般这个消息是被放入TCP栈的缓冲区。再退一步讲,即使目的机器的TCP栈后续也正常收到了该消息,并发送了确认数据包,也仅仅意味着消息到达了对方机器的协议栈,而不能认为消息被目标应用程序进程接收到并正确处理了。当消息发送过程中出现宕机等异常时,TCP栈缓冲区中的消息有可能丢失从而无法被目标节点正确处理。更有甚者,在网络中断前,某数据包已经被目标进程正确处理,之后网络立刻中断,由于接收方的TCP栈发送的确认数据包始终被丢失,发送方的TCP栈也有可能告知发送失败。另一方面,TCP只能保证同一个TCP链接内的网络消息不乱序,TCP链接之间的网络消息顺序则无法保证。但在分布式系统中,一个节点向另一个节点发送数据,有可能是先后使用多个TCP链接发送,也有可能是同时并发多个TCP链接发送,那么发送进程不能认为TCP系统先调用发送的消息就一定会先于后发送的消息到达对方节点并被处理。

本章小结

本章介绍了节点上的进程、线程、纤程的概念及其之间的关系。在实际编程中,大多数编程语言都提供了线程。本章也以Java为例提供了线程编程的范例。

在本章的最后探讨了节点之间的通信。网络通信往往是不可靠的,因此设计分布式系统时需要考虑众多的通信异常的场景。

写在最后

最近我整理了整套《JAVA核心知识点总结》,说实话,作为一 名 Java 程序员,不论你需不需要面试都应该好好看下这份资料。拿到手总是不亏的~我的不少粉丝也因此拿到腾讯字节快手offer,点击下面图片↓直达领取

好了,以上就是本文的全部内容了,如果觉得有收获,记得三连,我们下期再见。

以上是关于分布式系统中节点上的进程线程纤程概念及其之间关系你懂吗?的主要内容,如果未能解决你的问题,请参考以下文章

关于进程线程协程管程纤程超线程的对比理解

Linux中进程与线程的概念以及区别

Linux中进程与线程的概念以及区别

13-Linux中进程与线程的概念以及区别

多线程的秘密

nodejs中的fiber(纤程)库详解