在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
1. C/C++ 与 Go语言的“价值观”对照之前看过 白明老师 在GopherChina2017的一篇演讲文章《Go coding in go way》,里面提到C/C++/Go三门语言价值观,感觉很有意思,分享给大家感受一下: C的价值观摘录
C++价值观摘录
Go价值观
用一句话概括Go的价值观: Go is about orthogonal composition of simple concepts with preference in concurrency(Go是在偏好并发的环境下的简单概念/事物的正交组合). 从Go的价值观介绍可以看出 Go很适合并发编程,可以说其是为并发而生的一门语言,那它的并发机制如何?这正是这篇文章想要介绍的。 2. 从线程实现模型说起线程的实现模型主要有3种:内核级线程模型、用户级线程模型和混合型线程模型。它们之间最大的区别在于线程与内核调度实体KSE(Kernel Scheduling Entity)之间的对应关系上。所谓的内核调度实体KSE 就是指可以被操作系统内核调度器调度的对象实体,有些地方也称其为内核级线程,是操作系统内核的最小调度单元。 2.1 内核级线程模型用户线程与KSE是1对1关系(1:1)。大部分编程语言的线程库(如linux的pthread,Java的java.lang.Thread,C++11的std::thread等等)都是对操作系统的线程(内核级线程)的一层封装,创建出来的每个线程与一个不同的KSE静态关联,因此其调度完全由OS调度器来做。这种方式实现简单,直接借助OS提供的线程能力,并且不同用户线程之间一般也不会相互影响。但其创建,销毁以及多个线程之间的上下文切换等操作都是直接由OS层面亲自来做,在需要使用大量线程的场景下对OS的性能影响会很大。 2.2 用户级线程模型用户线程与KSE是多对1关系(M:1),这种线程的创建,销毁以及多个线程之间的协调等操作都是由用户自己实现的线程库来负责,对OS内核透明,一个进程中所有创建的线程都与同一个KSE在运行时动态关联。现在有许多语言实现的 协程 基本上都属于这种方式。这种实现方式相比内核级线程可以做的很轻量级,对系统资源的消耗会小很多,因此可以创建的数量与上下文切换所花费的代价也会小得多。但该模型有个致命的缺点,如果我们在某个用户线程上调用阻塞式系统调用(如用阻塞方式read网络IO),那么一旦KSE因阻塞被内核调度出CPU的话,剩下的所有对应的用户线程全都会变为阻塞状态(整个进程挂起)。 2.3 混合型线程模型用户线程与KSE是多对多关系(M:N), 这种实现综合了前两种模型的优点,为一个进程中创建多个KSE,并且线程可以与不同的KSE在运行时进行动态关联,当某个KSE由于其上工作的线程的阻塞操作被内核调度出CPU时,当前与其关联的其余用户线程可以重新与其他KSE建立关联关系。当然这种动态关联机制的实现很复杂,也需要用户自己去实现,这算是它的一个缺点吧。Go语言中的并发就是使用的这种实现方式,Go为了实现该模型自己实现了一个运行时调度器来负责Go中的"线程"与KSE的动态关联。此模型有时也被称为 两级线程模型,即用户调度器实现用户线程到KSE的“调度”,内核调度器实现KSE到CPU上的调度。 三种模型的示意图如下: 3. Go并发调度: G-P-M模型3.1 G-P-M模型有了上面的认识,我们可以开始真正的介绍Go的并发机制了,先用一段代码展示一下在Go语言中新建一个“线程”(Go语言中称为Goroutine)的样子: // 用go关键字加上一个函数(这里用了匿名函数)
// 调用就做到了在一个新的“线程”并发执行任务
go func() {
// do something in one new goroutine
}()
功能上等价于Java8的代码: new java.lang.Thread(() -> {
// do something in one new thread
}).start();
可以看到Go的并发用起来非常简单,用了一个语法糖将内部复杂的实现结结实实的包装了起来。其内部可以用下面这张图来概述: 其图中的G, P和M都是Go语言运行时系统(其中包括内存分配器,并发调度器,垃圾收集器等组件,可以想象为Java中的JVM)抽象出来概念和数据结构对象: 3.2 调度过程Go运行时完整的调度系统是很复杂,很难用一篇文章描述的清楚,这里只能从宏观上介绍一下,让大家有个整体的认识。 // Goroutine1
func task1() {
go task2()
go task3()
}
假如我们有一个G(Goroutine1)已经通过P被安排到了一个M上正在执行,在Goroutine1执行的过程中我们又创建两个G,这两个G会被马上放入与Goroutine1相同的P的本地G任务队列中,排队等待与该P绑定的M的执行,这是最基本的结构,很好理解。 关键问题是: 4. Goroutine与Channel: 锁之外的另一种同步机制在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。 // 声明channel变量
var syncChan = make(chan int) // 无缓冲channel,主要用于两个Goroutine之间建立同步点
var cacheChan = make(chan int, 10) // 缓冲channel
// 向channel中写入数据
syncChan <- 1
cacheChan <- 1
// 从channel读取数据
var i = <-syncChan
var j = <-cacheChan
几乎等价于的Java中的操作: TransferQueue<Integer> syncQueue = new LinkedTransferQueue<Integer>();
BlockingQueue<Integer> cacheQueue = new ArrayBlockingQueue<Integer>(10);
syncQueue.transfer(1);
cacheQueue.put(1);
int i = syncQueue.take();
int j = cacheQueu.take();
使用场景 5. Go语言对网络IO的优化在谈论高性能网络IO编程时,我们几乎都离不开epoll/kqueue/iocp等技术术语了,如Java最新的的NIO,Node等等的高性能网络IO模型都是基于这些技术实现的。诞生于21世纪,有互联网时代的C语言之称的Go语言,这么重视高并发,当然不会放过对网络的优化。且Go语言中对网络IO的优化很巧妙,让你可以用和以前一样的(同步的)思维方式去编程的同时(而不是反人类的异步方式),还能享受到与异步方式几乎同等高效的运行性能。那Go语言中是如何做的呢?主要是从两方面下手的: 6. 总结Go语言并不完美,它是以软件工程为目的的语言设计。其实现的并发机制也并不是什么革新的技术,只是将这些经典的理论和技术以一种简洁高效的方式组合了起来,并用简单抽象的API或语法糖开放给开发人员,着实减轻了开发人员编程的心智负担。而且其通过引入channel机制,将另一种并发编程模型(CSP: 通信顺序进程)带给了我们,给我们提供了使用其他并发编程思维方式的机会(有关CSP模型建议大家看看《七周七并发模型》这本书的第六章),Goroutine与Channel的组合是一对很有powerful的并发工具,相信其可以给你带了更好的并发编程体验。 7. 参考《Go并发编程实战》 第2版 转载自:https://github.com/k2huang/blogpost/blob/master/golang/并发编程/并发机制/Go并发机制.md |
请发表评论