一、线程的实现
线程的实现方式主要有三种:内核线程实现、用户线程实现、用户线程加轻量级进程混合实现。因为自己只对java的线程比较熟悉一点,所以主要针对java线程和go的协程之间进行一个对比。
线程模型主要有三种:1、内核级别线程;2、用户级别线程;3、混合线程
1、内核级别线程
内核级别线程就是直接由操作系统的内核(kernel)支持的线程,这种方式实现的线程主要通过内核的调度器来进行调度,由内核完成线程切换。一般来讲程序不会直接调用系统内核线程,而是利用内核线程的一种高级接口-轻量级进程(Light Weight Process,即LWP,它也可以视为用户线程),也就是我们平时所说的线程,每一个LWP都是由一个内核线程支持,也就是先有内核线程,再有LWP。这种LWP与内核线程之间1:1的关系称为一对一线程模型,这是一种最简单的线程实现方式。见下图:
2018-11-16 22-40-01 的屏幕截图.png
这种方式创建的每一个线程都需要由一个内核线程支持,需要消耗一定的内核资源,因此一个系统支持的线程数量是有限的。另外由于基于内核线程实现,这种方式创建的线程操作需要进行系统调用,而系统调用代价较高,需要在用户态和内核态进行切换(这个我也不懂)。
2、用户级别线程
这里所指的用户级线程主要是创建在用户空间的线程库上,系统内核感受不到线程的实现方式。用户线程的建立、同步、销毁等在用户态中完成,不需要内核的介入。这种进程和用户线程(UT)之间1:N的关系称为一对多线程模型。
1321313.jpg
这种方式的优势就是上下文切换比较快,缺点是无法从多线程处理器或多处理器计算机上的硬件加速中受益,同时调度的线程永远不会超过一个。
3、混合线程
这种方式相当于是第一种方式和第二种方式的混合,即有LWT,也有用户线程,这种方式中用户线程(UT)和LWT的数量比是不定的,即所谓的N:M关系,也就是所谓的多对多模型。这种实现线程的方式相比前两种也更为复杂,这种方式中由线程库负责在可用的可调度实体上调度用户线程,这使得线程的上下文切换非常快,因为它避免了系统调用。但是增加了复杂性和优先级倒置的可能性,以及在用户态调度程序和内核调度程序之间没有广泛(且高昂)协调的次优调度。
3213213123.jpg
java线程实现
主要说下常用的hotspot的JVM,采用的是第一种1:1的线程模型,即:map a java thread to a native thread,也就是说java线程会和native线程有个一一映射的关系,如果看下java的Thread类就可以发现有很多的native方法,这就涉及到操作系统的线程了。
二、go语言并发模式
go语言支持两种并发模式,一种是Communicating Sequential Processes(CSP)模式,这种模式中值是在相互独立的协程(goroutine)中传递的,协程和协程之间使用到就是上次说到的channel。另外一种就是我们比较传统的模式,也是我们相对熟悉的模式Share Memory Multithreading。但是go语言推荐的还是第一种模式,go官网文档是说:Do not communicate by sharing memory; instead, share memory by communicating.也就是说不建议线程或协程之间通过共享内存通讯,而是通过通讯共享内存。比方说比较熟悉的java其实就是共享内存模式的并发模式,在涉及到多线程的问题时,必须考虑共享数据的安全性。
三、线程和协程之间的区别
这里说的协程指的只是go语言的goroutine。线程和协程的区别主要是数量上的,而不是性质上,所以说协程从逻辑上来说也是线程。
栈的大小:
1、线程栈
操作系统的线程一般都分配有一块固定大小的内存块(一般来说大小是2M,这个需要查证),我查找资料显示的64位Linux上,hotspot虚拟机的栈的大小默认为1M,地址:https://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html。。栈存储的是方法的局部变量或者一些基本数据类型(java中)。因为栈的大小是固定的,在执行某些方法的时候可能就不太够用,比如一些比较复杂或则一些深的递归操作,比如熟悉的java有时候会有栈溢出异常;当然某些时候2M可能显得有点大,这样从一定程度上来说又造成了浪费。
2、协程
协程开始的时候也会分配一定大小的内存区域,一般只有2K,和线程的栈一样,协程的栈存储的也是局部变量,但是不同的是协程的栈的大小是不固定的,是可以根据需要自动调整大小的,最大甚至可以达到1G,所以灵活性非常好。
调度方式
1、系统级别的线程是由操作系统的内核(kernel)调度的,每过几毫秒,硬件的计时器就会中断处理器,从而引起被成为调度器的内核函数执行。调度器会暂停当前正在执行的线程,并把它的寄存器存到内存中,并查看线程列表决定接下来执行哪一个线程,然后从内存中恢复改线程的寄存器,最后恢复该线程的执行。因为线程是通过内核调度的,从一个线程切换到另一个线程就涉及到上下文转换,建议看下维基百科:https://en.wikipedia.org/wiki/Context_switch。简单说就是将一个用户线程的状态保存到内存,恢复另一个用户线程的状态,并且更新调度程序的数据结构,导致上下文切换比较慢。
2、go语言使用了所谓的N:M调度的技术实现了自己的调度器,它在N个系统线程上多路复用(或调度)M个协程,也就是说由n个系统线程,生成了m个go的协程(可以这么理解吧?)。go语言的调度工作类似于系统内核的调度,但是它只关注单个go程序的协程。
另外一点就是go的协程是没有标识的,在java中当前执行的线程都会有一个唯一的标识,它的好处就是可以很容易的就构建出一个抽像的"thread-local storage",即java中的ThreadLocal,每个线程都可以创建出这样一个数据结构存储只属于当前线程的一些变量。但是goroutine并不支持,因为ThreadLocal可能会被滥用。go语言提倡的是一种更简单的编程方式,即参数影响函数的行为应该是显性的。
go因为在创建协程的数量上一般没有特别的限制,所以可以很轻松的创建出很多个协程出来,而java因为采用的是1:1的线程模型,线程数量特别是并发线程数会受到CPU和操作系统的限制(我记得java线程池会获取当前可使用的CPU核数,可能有误),所以并发性能上应该不如go语言,有人也说go语言天生就带有高并发光环加持。这里无意区分java和go孰优孰劣,只是想从线程和协程的实现上来简单的了解下二者的差别。
作者:非典型_程序员
链接:https://www.jianshu.com/p/6168b10dee34
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
java线程和go协程 - 简书