Goroutine调度器(二):调度流程简述

我们都知道Go语言是原生支持语言级并发的,这个并发的最小逻辑单元就是goroutine。goroutine就类似于Go语言提供的一种“用户态线程”,当然这种“用户态线程”是跑在内核级线程之上的。当我们创建了很多的goroutine,并且它们都是跑在同一个内核线程之上的时候,就需要一个调度器来维护这些goroutine,确保所有的goroutine都使用CPU,并且是尽可能公平的使用CPU资源。

这个调度器的原理以及实现值得我们去深入研究一下。支撑整个调度器的主要有4个重要结构,分别是P、M、G、Sched,前三个定义在runtime.h中,Sched定义在proc.c中。

  • Sched结构就是调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。
  • M代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息。
  • P全称是Processor,处理器,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine,这个P的角色可能有一点让人迷惑,一开始容易和M冲突,后面重点聊一下它们的关系。
  • G就是goroutine实现的核心结构了,G维护了goroutine需要的栈、程序计数器以及它所在的M等信息。

理解M、P、G三者的关系对理解整个调度器非常重要,我从网络上找了一个图来说明其三者关系:

地鼠用小车运着一堆待加工的砖。M就可以看作图中的地鼠,P就是小车,G就是小车里装的砖。一图胜千言啊,弄清楚了它们三者的关系,下面我们就开始重点聊地鼠是如何在搬运砖块的。 Continue reading “Goroutine调度器(二):调度流程简述”

Goroutine调度器(一):P、M、G关系

在了解Go的运行时的scheduler之前,需要先了解为什么需要它,因为我们可能会想,OS内核不是已经有一个线程scheduler了嘛?
熟悉POSIX API的人都知道,POSIX的方案在很大程度上是对Unix process进场模型的一个逻辑描述和扩展,两者有很多相似的地方。 Thread有自己的信号掩码,CPU affinity等。但是很多特征对于Go程序来说都是累赘。 尤其是context上下文切换的耗时。另一个原因是Go的垃圾回收需要所有的goroutine停止,使得内存在一个一致的状态。垃圾回收的时间点是不确定的,如果依靠OS自身的scheduler来调度,那么会有大量的线程需要停止工作。 Continue reading “Goroutine调度器(一):P、M、G关系”

gf框架之并发安全容器 – gmap,以及与sync.Map的性能比较

gf框架提供了几个非常实用的并发安全容器,其中gmap就是项目开发中最常用的一个。

gmap具体的方法请参考godoc:https://godoc.org/github.com/johng-cn/gf/g/container/gmap

gmap内部有多个类型结构体定义,包括:IntBoolMapIntIntMapIntInterfaceMapIntStringMapInterfaceInterfaceMapStringBoolMapStringIntMapStringInterfaceMapStringStringMapUintInterfaceMap

从执行效率上考虑,基于不同的需求场景,选择合适的类型结构体,其执行效率是不一样的,以下使用基准测试来对比各个类型的写入性能(测试代码):

Continue reading “gf框架之并发安全容器 – gmap,以及与sync.Map的性能比较”

gkvdb v2.0发布,性能改进及高可用版本

项目地址:https://gitee.com/johng/gkvdb

距离在开源中国发布的gkvdb v1.81已经过去了近3个月,在这三个月中gkvdb也变得越来越成熟稳定,本次发布最大的两点在于gkvdb的性能改进以及高可用特性。

其中,性能改进主要是针对于gf框架改进的一些对应的协调改进工作,以及将同步机制从定时同步调整为使用golang的channel实现的实时同步,提高了同步的效率。

高可用特性保证了在任何的情况下,只要API调用成功,那么这条数据即是成功,处理结果在任何异常情况下也不会发生改变,该特性使得gkvdb可以被推广使用到更复杂的生产环境中,保证写入数据的稳定可靠。特别是,gkvdb经过了严格的并发安全测试,在高并发的环境下也表现得非常出色。

自己的博客,本次发布信息也就这些吧。

gf框架之gparser – 强大灵活的数据格式编码/解析包

gf框架针对常用的数据格式编码解析,提供了异常强大灵活的功能,由gparser包提供,支持Go变量(interface{})、Struct、JSON、XML、YAML/YML、TOML数据格式之间的相互转换,支持按照层级进行数据检索访问、支持运行时动态新增/修改/删除层级变量(并发安全)等特性。gparser包使得对于未知数据结构、多维数组结构的访问、操作变得异常的简便。
Continue reading “gf框架之gparser – 强大灵活的数据格式编码/解析包”

gf框架之gvalid – 强大灵活的数据校验/表单校验模块

gf提供了非常强大易用的数据校验功能,通过gvalid包提供,封装了40种常用的校验规则,支持单数据多规则校验、多数据多规则批量校验、自定义错误信息、自定义正则校验等特性。由于gf是模块化、低耦合设计,gvalid包也可以在项目中单独引入使用。

使用方式:

Continue reading “gf框架之gvalid – 强大灵活的数据校验/表单校验模块”

gf框架之grpool – 高性能的goroutine池

Go语言中的goroutine虽然相对于系统线程来说比较轻量级,但是在高并发量下的goroutine频繁创建和销毁对于性能损耗以及GC来说压力也不小。充分将goroutine复用,减少goroutine的创建/销毁的性能损耗,这便是grpool对goroutine进行池化封装的目的。例如,针对于100W个执行任务,使用goroutine的话需要不停创建并销毁100W个goroutine,而使用grpool也许底层只需要几千个goroutine便能充分复用地执行完成所有任务。经测试,在高并发下grpool的性能比原生的goroutine高出几倍到数百倍!并且随之也极大地降低了内存使用率。

性能测试报告:http://johng.cn/grpool-performance/ Continue reading “gf框架之grpool – 高性能的goroutine池”

Go学习整理笔记

最近整理一下Go学习中的一些要点,包括Go性能优化及底层源码要点,作此笔记,巩固基础。

一、并发

1、Go语言的goroutine类似于线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力;

2、通常情况下,用多进程来实现分布式负载均衡,减轻单进程垃圾回收压力;用多线程(LWP)抢夺更多的处理器资源;用协程来提高处理器时间片利用率;

3、相比较系统默认MB级别的线程栈,goroutine自定义栈初始仅需2KB,所以才能创建成千上万的并发任务。自定义栈采用按需分配策略,在需要时进行扩容,最大能到GB规模;

4、Go在运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。该量默认与处理器核数相等,可用runtime.GOMAXPROCS函数或者环境变量修改;

5、通道:

         1)、通道(channel)相当于一个并发安全的队列;

         2)、goroutine leak是指goroutine处于发送或者接受阻塞状态,但一直未被唤醒,垃圾回收器并不收集此类资源,导致它们会在队列里长期休眠,形成资源泄露;

6、同步:

         1)、通道并不是用来取代锁的,它们有各自不同的使用场景。通道倾向于解决逻辑层次的并发处理架构,而锁则用来保护局部范围的数据安全;

         2)、将Mutex作为匿名字段时,相关方法必须实现为pointer-receiver模式,否则会因为复制导致锁机制失效;

         3)、应将Mutex锁粒度控制在最小范围内,及早释放;

7、建议:

         1)、对性能要求较高时,应避免使用defer Unlock;

         2)、读写并发时,用RWMutex性能会更好一些;

         3)、对单个数据读写保护,可尝试用原子操作;

         4)、执行严格测试,尽可能打开数据竞争检查;

Continue reading “Go学习整理笔记”

GO性能优化小结

1 内存优化

1.1 小对象合并成结构体一次分配,减少内存分配次数

做过C/C++的同学可能知道,小对象在堆上频繁地申请释放,会造成内存碎片(有的叫空洞),导致分配大的对象时无法申请到连续的内存空间,一般建议是采用内存池。Go runtime底层也采用内存池,但每个span大小为4k,同时维护一个cache。cache有一个0到n的list数组,list数组的每个单元挂载的是一个链表,链表的每个节点就是一块可用的内存,同一链表中的所有节点内存块都是大小相等的;但是不同链表的内存大小是不等的,也就是说list数组的一个单元存储的是一类固定大小的内存块,不同单元里存储的内存块大小是不等的。这就说明cache缓存的是不同类大小的内存对象,当然想申请的内存大小最接近于哪类缓存内存块时,就分配哪类内存块。当cache不够再向spanalloc中分配。

建议:小对象合并成结构体一次分配,示意如下:

替换为:

Continue reading “GO性能优化小结”