热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

解密高并发支持

通过讲述JavaThread、Erlangprocess、Golanggoroutine的详细机制阐述并发的秘密,解密Erlang和Golang的高并发支持。Erlang和Gola

通过讲述Java Thread、Erlang process、Golang goroutine的详细机制阐述并发的秘密,解密Erlang和Golang的高并发支持。

       Erlang和Golang都提供并发支持,能够支持几万甚至几十万的并发,但是Java或是C/C++在线程达到几千的时候,CPU性能就开始明显的下降,所以在并发度上来看,Java或是C/C++相对于Erlang、Golang简直就是弱爆了。

       是什么让Erlang、Golang能够支持几万几十万的并发呢?接下来为你一一讲述。

一、Java Thread线程

线程的定义:线程(thread, 台湾称“执行绪”)是进程中某个单一顺序的控制流。也被称为轻量进程(lightweight processes)(来自百度百科)。

一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪阻塞运行三种基本状态。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

在上述的线程简介中有一个关键是系统独立调度和分派,系统可以是操作系统,也可以是用户系统,就是线程可以由操作系统调度,也可以由用户自定义实现调度,Java 中的 Thread就是由操作系统调度的,这意味着CPU需要去调度用户创建的Java Thread,所以Java Thread是内核级线程,每个Thread都将去抢占CPU。

内核级线程包含许许多多系统指令,如进入内核需要中断指令,需要保存现场等,以至于内核级别的线程会占用一定量的资源,这个资源比起用户级线程大的多,一个内核级线程占去的资源可能是用户级线程的几十上百倍,Java Thread创建时占用的内存默认为1M。

Java Thread的状态:

1     新生状态(New): 当一个线程的实例被创建即使用new关键字和Thread类或其子类创建一个线程对象后,此时该线程处于新生(new)状态,处于新生状态的线程有自己的内存空间,但该线程并没有运行,此时线程还不是活着的(notalive);

  2    就绪状态(Runnable): 通过调用线程实例的start()方法来启动线程使线程进入就绪状态(runnable);处于就绪状态的线程已经具备了运行条件,但还没有被分配到CPU即不一定会被立即执行,此时处于线程就绪队列,等待系统为其分配CPCU,等待状态并不是执行状态; 此时线程是活着的(alive);

  3    运行状态(Running): 一旦获取CPU(被JVM选中),线程就进入运行(running)状态,线程的run()方法才开始被执行;在运行状态的线程执行自己的run()方法中的操作,直到调用其他的方法而终止、或者等待某种资源而阻塞、或者完成任务而死亡;如果在给定的时间片内没有执行结束,就会被系统给换下来回到线程的等待状态;此时线程是活着的(alive);

  4    阻塞状态(Blocked):通过调用join()、sleep()、wait()或者资源被暂用使线程处于阻塞(blocked)状态;处于Blocking状态的线程仍然是活着的(alive)

  5    死亡状态(Dead):当一个线程的run()方法运行完毕或被中断或被异常退出,该线程到达死亡(dead)状态。此时可能仍然存在一个该Thread的实例对象,当该Thready已经不可能在被作为一个可被独立执行的线程对待了,线程的独立的callstack已经被dissolved。一旦某一线程进入Dead状态,他就再也不能进入一个独立线程的生命周期了。对于一个处于Dead状态的线程调用start()方法,会出现一个运行期(runtimeexception)的异常;处于Dead状态的线程不是活着的(notalive)。

在Java进程中,处于运行状态的线程可以是很多的,Java中线程进入CPU是采用抢占式的,一个CPU只能有一个线程进入,在当前多核的处理器下允许与核数相同的线程同时进入CPU。

 

并发编程的模型:

并发编程中需要处理线程通信和线程同步。许多情况下线程之间的通信机制有共享内存和消息传递。

在共享内存的模型中,线程之间共享程序的数据和状态,线程之间通过读写内存中的数据和状态来隐士的进行通信。消息传递的并发模型中,线程没有公共的的共享数据和状态,必须通过明确的发送消息来进行通信。

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java的并发是采用共享内存模型实现的,Java Thread的通信是隐士进行的。Java线程之间的通信有Java内存模型控制,决定一个线程对共享变量的写入何时对另一个线程可见。

Java线程之间共享的变量是存储在主存的中,当然Java每个线程还有自己私有的内存空间,该内存空间保存了共享变量的副本。Java Thread共享内存通信模型如下图:

(图形来自网络)


从上图来看,线程A和线程B通信需要经历下面两个过程:

1         线程A将私有空间的共享变量的副本数据刷入到主存;

2         线程B从主存读取共享变量的数据到私有空间;

Java中会对指令做重排序,用以优化提高性能,不详述。

 

       Happens-before

       Happens-before意思是如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系,两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered beforethe second)。

       happens-before规则如下:

1        程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。

2        监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。

3        volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。

4        传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

 

Java Thread都是内核级线程,每个线程都会去抢占CPU,线程在用户态与内核态的切换的开销比较大,线程数量上升到一定程度的时候,线程上下文的切换将占用大量资源,线程切换占去大量CPU,于是性能会下降的很快,同时Java线程本身占用的资源也导致不能使用大量的线程,比如上万线程。


二、Erlang process进程

Erlang也是使用虚拟机的模式来获得可移植性,也使用了垃圾回收机制来省去程序员内存管理的麻烦。

Erlang process是运行于Erlang虚拟机之中的类似Java Thread线程的最小单位,姑且叫他E线程,E线程其实是伪线程,实际上不仅仅E线程是伪线程,很多轻量级线程都是伪线程,所谓伪线程是指运行在用户态的线程,伪线程是用户的执行逻辑,不会直接调用内核来进入内核运行。

轻量级线程不直接调用内核进入内核态,那么轻量级线程是怎么运行的呢?这就需要调度器来实现。

首先看看Erlang的内存机制,Java虚拟机维护了一个可以被所有线程使用和共享的堆,占用了虚拟机的大部分内存,包括虚拟机的一些特殊数据区域,例如代码缓存和永久区,这些都是被所有线程共享的。Erlang中使用了私有堆的概念,每个E线程都有自己的小堆,这个堆里面包含了这个E线程使用到的数据以及线程栈,这个堆是在E线程创建的时候分配的,当E线程结束后,Erlang虚拟机就会将这个私有堆回收掉。除了私有堆以外,Erlang中还有二进制堆和消息堆,二进制堆分配了大量的数据块用来线程之间共享数据,比如文件输入、网络缓冲区。消息堆中存放的是消息数据。

与Java线程的隐士通信机制不同的是,Erlang的E线程通信是采用消息传递的方式实现通信的,是从发送线程复制一份数据到接收线程,这些发送的数据就是存放在消息堆中的。Erlang的E线程通信是完全异步的,Erlang将消息发送到消息堆中则完成,有另外的E线程去读取该消息,消息队列不抑制消息的增长。

Erlang中因为有私有堆的存在,Erlang线程对自己的数据检查不需要采用任何形式的锁,并且避免了破坏性的写,只有E线程自身会去写私有堆,这样就完全没有必要对共享数据加锁。

Erlang的缺点是没有抑制内存增长的机制,当Erlang线程积累了大量数据的时候,Erlang虚拟机会重新分配空间,扩大私有堆,然而,这个重新分配的算法会导致堆空间急速增长。

“私有堆”是一个非常强大的工具。它避免了实时系统里的锁机制,这个意味着它将比java更具扩展性。而java的硬性限制内存的模型,则能在你的系统压力剧增,或是遭受DDOS攻击的时候保持稳定。

 

Erlang线程是处在用户态层面的线程,那么Erlang本身就要实现对process的调度。

Erlang一般情况是在计算机的每个核心上运行一个线程,这个线程是内核级别的线程,同时这个线程就是process的调度器。调度器无法在多个核心之间切换,每个调度器都是绑定了指定计算机核心的。

Erlang创建的轻量级线程process是非常小的,一般不到1k,根据不同的系统有所区别,创建的process都将放入调度器的队列中,当process获得了时间片以后就可以在调度器绑定的核心上运行,所以process的切换是用户态下进行的,与Java Thread的内核切换是不同的。


Erlang的process在进入调度器的队列后可能并不一定在调度器绑定的CPU核心中运行,可能发生迁移,这种迁移是为了保证计算机的所有CPU能够得到充分的利用,调度器并不是一直运行在CPU上的,可能会进入睡眠状态,当调度器队列中有process需要处理的时候就被唤醒,抢占CPU,执行process,同时调度器在队列上所有process执行完后也不是马上进入睡眠状态的,调度器会去其他调度器上获取process,并且即使调度器从其他调度器上拿不到process也不会马上进入睡眠状态,而是再等待一段时间,如果还是没有process进来,调度器才会进入睡眠状态。

       Erlang将延时看的比吞吐量更加重要,更多需要的是低延时的处理,Erlang调度器在抢占到CPU后,可能会长时间的占有CPU,然后调度器让队列中的process使用CPU进行处理。

       从总体来说是每个process都会进入内核进行调用,但是每个process并不是直接的内核线程,process进入内核是由调度器帮助进行的。


三、Golang goroutine去程

Golang中的并发单位是goroutine暂且称为去程。

Goroutine也是轻量级线程,可以在一个进程中创建执行数十万的goroutine,与Erlang的process一样是用户态下的伪线程,同样相对于Java而言,用户态的线程可以大量创建,大量创建的goroutine不会导致CPU忙于上下文的切换,goroutine的切换是在用户态下实现的。Goroutine负责的是代码执行,那么goroutine之间的通信如何实现呢?Golang中还有一种称为channel的单位,暂且称为程道,程道负责的是goroutine之间消息或者说事件的传递。

程道是协程之间的数据传输通道。程道可以在众多的去程之间传递数据,具体可以值也可以是个引用。程道有两种使用方式。

1        去程可以试图向程道放入数据,如果程道满了,会挂起去程,直到程道可以为他放入数据为止。

2        去程可以试图向程道索取数据,如果程道没有数据,会挂起去程,直到程道返回数据为止。

如此,程道就可以在传递数据的同时,控制去程的运行。有点像事件驱动,也有点像阻塞队列。

这两个概念非常的简单,各个语言平台都会有相应的实现。在Java和C上也各有库可以实现两者。

Golang

Erlang

Scala(Actor)

去程 

goroutines

process

actor

消息队列

channel

mailbox

channel

对于一个goroutine来说,它其中变量的读, 写操作执行表现必须和从所写的代码得出的预期是一致的。也就是说,在不改变程序表现的情况下,编译器和处理器为了优化代码可能会改变变量的操作顺序即: 指令乱序重排。但是在两个不同的goroutine对相同变量操作时, 会因为指令重排导致不同的goroutine对变量的操作顺序的认识变得不一致。例如,一个goroutine执行a = 1; b = 2;,在另一个goroutine中可能会现感知到变量b先于变量a被改变。

为了解决这种二义性问题,Go语言中引进一个happens before的概念,它用于描述对内存操作的先后顺序问题。如果事件e1 happens before 事件 e2,我们说事件e2 happens after e1。如果,事件e1 does not happen before 事件 e2,并且 does not happen after e2,我们说事件e1和e2同时发生。

对于一个单一的goroutine,happens before 的顺序和代码的顺序是一致的。

如果能满足以下的条件,一个对变量v的读事件r可以感知到另一个对变量v的写事件w:

  1. 写事件w happens before 读事件r。
  2. 没有既满足 happens after w 同时满主 happens before r 的对变量v的写事件w。

为了保证读事件r可以感知对变量v的写事件,我们首先要确保w是变量v的唯一的写事件。同时还要满足以下条件:

  1. 写事件w happens before 读事件r。
  2. 其他对变量v的访问必须 happens before 写事件w 或者 happens after 读事件r。

第二组条件比第一组条件更加严格。因为,它要求在w和 r并行执行的程序中不能再有其他的读操作。

对于在单一的goroutine中两组条件是等价的,读事件可以确保感知到对变量的写事件。但是,对于在 两个goroutines共享变量v,我们必须通过同步事件来保证 happens-before 条件 (这是读事件感知写事件的必要条件)。

将变量v自动初始化为零也是属于这个内存操作模型。

读写超过一个机器字长度的数据,顺序也是不能保证的。

(以上happens before解释是来自Google的说明)

 

Goroutine是运行在用户态的线程,那么同样需要对goroutine实现相应的调度器。Golang中调度器负责调度goroutine在内核级线程上执行。

Golang中有内核级线程M、处理器Processer、去程G

·        M代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息。

·        P全称是Processor,处理器,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine,这个P的角色可能有一点让人迷惑,一开始容易和M冲突,后面重点聊一下它们的关系。

·        G就是goroutine实现的核心结构了,G维护了goroutine需要的栈、程序计数器以及它所在的M等信息。

P是一组goroutine的组合,进程中P的数量可以由设定GOMAXPROCS参数来配置数量,进程中P的数量默认最大是256,就是当GOMAXPROCS超过256的时候,进程中的P同样是只有256个。

Golang中的内核级线程是由Go虚拟机创建的,用户不能显式的创建内核级线程,用户只能创建goroutine,M内核线程是Go虚拟机根据实际情况来创建的,是调用clone系统调用来创建内核线程,M的创建同时会分配空闲的P。

调度器调度内核线程M会从P中获取需要执行的goroutine,取不到goroutine的时候会从其他内核线程M的P中获取goroutine,甚至于调度会将M的P中的goroutine重新分发到其他的M的P中,也可能会创建新的内核线程M。

与Erlang不同的是,Golang的内核线程是可以配置多个的,而Erlang一般是根据CPU的数量来创建内核线程的。

Goroutine上下文的切换也是用户态下的,不会涉及到内核线程的切换,因为Golang中可能创建有许多的内核线程,数量最大为256,所以Golang也存在内核线程的切换。

 



从Java Thread到Erlang process,再到Golang goroutine,分别讲述了他们的区别以及内存模型,如何并发,如何通信的,对于Erlang和Golang的设计来看,确实是比Java Thread更加震撼,尽量的减少对CPU资源的浪费,确实是提高了他们的并发度。

许多高并发语言都是通过所谓的轻量级线程,即用户态线程,实现了高层并发,减少了线程切换的开销以及线程自身的开销。Akka.Actor、Scala.Actor等支持的高并发也是高层实现的并发支持。

这里并不否认Java Thread自己本身的特点,Erlang、Golang高并发支持并不是适用于所有人,更多还是需要自己根据实际情况选择。



推荐阅读
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • ShiftLeft:将静态防护与运行时防护结合的持续性安全防护解决方案
    ShiftLeft公司是一家致力于将应用的静态防护和运行时防护与应用开发自动化工作流相结合以提升软件开发生命周期中的安全性的公司。传统的安全防护方式存在误报率高、人工成本高、耗时长等问题,而ShiftLeft提供的持续性安全防护解决方案能够解决这些问题。通过将下一代静态代码分析与应用开发自动化工作流中涉及的安全工具相结合,ShiftLeft帮助企业实现DevSecOps的安全部分,提供高效、准确的安全能力。 ... [详细]
  • Mono为何能跨平台
    概念JIT编译(JITcompilation),运行时需要代码时,将Microsoft中间语言(MSIL)转换为机器码的编译。CLR(CommonLa ... [详细]
  • go channel 缓冲区最大限制_Golang学习笔记之并发.协程(Goroutine)、信道(Channel)
    原文作者:学生黄哲来源:简书Go是并发语言,而不是并行语言。一、并发和并行的区别•并发(concurrency)是指一次处理大量事情的能力 ... [详细]
  • 本文比较了eBPF和WebAssembly作为云原生VM的特点和应用领域。eBPF作为运行在Linux内核中的轻量级代码执行沙箱,适用于网络或安全相关的任务;而WebAssembly作为图灵完备的语言,在商业应用中具有优势。同时,介绍了WebAssembly在Linux内核中运行的尝试以及基于LLVM的云原生WebAssembly编译器WasmEdge Runtime的案例,展示了WebAssembly作为原生应用程序的潜力。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • Java 11相对于Java 8,OptaPlanner性能提升有多大?
    本文通过基准测试比较了Java 11和Java 8对OptaPlanner的性能提升。测试结果表明,在相同的硬件环境下,Java 11相对于Java 8在垃圾回收方面表现更好,从而提升了OptaPlanner的性能。 ... [详细]
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了软件测试知识点之数据库压力测试方法小结相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了Java调用Windows下某些程序的方法,包括调用可执行程序和批处理命令。针对Java不支持直接调用批处理文件的问题,提供了一种将批处理文件转换为可执行文件的解决方案。介绍了使用Quick Batch File Compiler将批处理脚本编译为EXE文件,并通过Java调用可执行文件的方法。详细介绍了编译和反编译的步骤,以及调用方法的示例代码。 ... [详细]
  • 都说Python处理速度慢,为何月活7亿的 Instagram依然在使用Python?
    点击“Python编程与实战”,选择“置顶公众号”第一时间获取Python技术干货!来自|简书作者|我爱学python链接|https:www.jian ... [详细]
author-avatar
靠谱的留一手_267
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有