go汇编入门

lrita 发表了文章 • 0 个评论 • 112 次浏览 • 1 天前 • 来自相关话题

当go get遇上gitlab

yhf_szb 发表了文章 • 0 个评论 • 250 次浏览 • 2017-11-30 23:15 • 来自相关话题

前言

go get命令可以说是golang开发者最常用的命令了,通过它我们可以轻松获得各种开源仓库中的包,并且比较方便的在不同的开发机快速部署开发环境。

此处应有版本依赖... 查看全部

前言


go get命令可以说是golang开发者最常用的命令了,通过它我们可以轻松获得各种开源仓库中的包,并且比较方便的在不同的开发机快速部署开发环境。



此处应有版本依赖的问题,但听说新版的go会处理。



但作为企业行为,不是所有的代码包都适合放在公开的网站上,而开源的又适用于中小型企业的自建git仓库工具中,gitlab无疑是耀眼的一个,如果配合docker,一键部署简直不要太舒服。


自建仓库的go get


其实golang在设计的时候是可以支持go get获取自建仓库的,详细原理网上很多,不罗嗦,简单讲,当执行go get your-web.com/your-project的时候,go其实会提交一个HTTP GET 到网址https://you-web.com/your-project?go-get=1,此时如果这个网址能在headmeta标签中返回以下格式的内容时,就可以告诉go客户端应该再到哪里获取仓库。



注意,默认情况下go会且仅会从https的网址获取数据!



<html>
<head>
<meta content="szyhf/go-dicache git https://github.com/szyhf/go-dicache" name="go-import">
</head>
</html>

其中meta标签中的name是必填项,内容必须是go-import,而contat的格式为导入路径 VCS类型 仓库路径,例如,上述代码的含义就是从https://github.com/szyhf/go-dicache下载git仓库并放到导入路径为szyhf/go-dicache的$GOPATH中。



至于域名怎么能访问,怎么输出这个meta,相信对于各位童鞋来说肯定是不是什么problem,跳过



更多说明可以看这里go get命令


遇上gitlab


那么对于自建仓库的gitlab,应该怎么实现这个功能呢?其实gitlab很早就支持了go get,例如你的gitlab网站部署在gitlab.hello.com,你要get的项目是gitlab.hello.com/foo/bar,那么直接执行go get gitlab.hello.com/foo/bar就可以了,gitlab会自动在返回的网页中设置合适的meta标签的。


但实际使用的时候,我们知道,很多时候我们之所以用自建的gitlab,是因为这个仓库见不得光,说白了,我们自己git clone的时候还需要输入一下密码,go get显然也绕不过这个问题。


而默认情况下,gitlab返回的meta标签中的url是https类型的,而实际上更多时候,我们都是通过ssh的方式实现获取仓库,因此,我们需要对gitlab做一定的改造。


当前笔者使用的gitlab版本是9.3.6,对go get的支持是用过ruby-rails中的Middleware的方式实现的,很传统,如果懂ruby的话可以试试直接改,文件是gitlab/embedded/service/gitlab-rails/lib/gitlab/middleware/go.rb,此处不多说。



主要考虑要改源代码不是很优雅,特别是要处理gitlab的升级的时候。



此处给一个不需要懂ruby的非侵入式方案,因为公司的gitlab是搭配nginx使用的,所以在处理对gitlab的请求的时候,加入以下配置,可以达到一样的效果:


if ($http_user_agent ~* "go") {
return 200 "<!DOCTYPE html><head><meta content='$host$uri git ssh://git@$host:$uri.git' name='go-import'></head></html>";
}

简单解释一下,来自go get的HTTP请求中,User Agent都是以go作为开头的,而且go也不会跟现在任何主流浏览器冲突,所以当发现$http_user_agentgo开头的时候,直接返回一个固定的字符串,字符串中注意仓库路径的拼接要加上ssh://,要不go1.8以下的版本无法识别。



上述是我第一次的方案,go get gitlab.hello.com/foo/bar成功,顺利按照预期工作。



然后工作了一阵子之后忽然又出现了新的问题,subpackage


原因很简单,当我们在go get某个项目时,如果这个项目依赖于gitlab.hello.com/foo/bar/you/hu包,那么go get实际提交的请求会变成https://gitlab.hello.com/foo/bar/you/hu,而实际上并不存在这个仓库,如果按方案1的实现逻辑,会尝试下载git@gitlab.hello.com/foo/bar/you/hu.git


很遗憾,这个仓库并不存在,真正存在的是gitlab.hello.com/foo/bar.git,那么应该怎么处理呢?结合nginx的正则表达式重定位的功能,更新的配置如下:


location ~* ^/[^/]+/[^/]+$ {
if ($http_user_agent ~* '^go.*') {
return 200 "<!DOCTYPE html><head><meta content='$host$uri git ssh://git@$host:$uri.git' name='go-import'></head></html>";
}
proxy_cache off;
proxy_pass http://gitlab-workhorse;
}
location ~* ^/(?<holder>[^/]+)/(?<project>[^/]+)/.*$ {
set $goRedirect 'https://$host/$holder/$project?$args';
if ($http_user_agent ~* '^go.*') {
return 301 $goRedirect;
}
proxy_cache off;
proxy_pass http://gitlab-workhorse;
}

其中proxy_cache off;proxy_pass http://gitlab-workhorse;是gitlab官方文档中给出的设置。



其实很容易理解。



主要来解释一下两个location,首先:


~*表示开始不区分大小写地匹配后边给出的正则。


正则^/[^/]+/[^/]+$是为了匹配形如/foo/bar的路径结构,如果匹配成功,继续检查User-Agent,如果符合go,则按第一个方案返回结果,如果不符合,则按一般的gitlab请求进行处理。


正则^/(?<holder>[^/]+)/(?<project>[^/]+)/.*$是为了匹配形如/foo/bar/you/hu/hu的结构,其中的小括号表示对其第一二个斜杠之间的字符串进行捕捉,并赋值给变量$holder$project,然后判定User-Agent,如果符合go,则将请求重定位给/foo/bar,也就会再交给第一个正则处理,最后获得一致的结果。


显然第二个方案比第一个方案复杂了不少,但也都是很标准的nginx配置逻辑,未必优雅,但还是很实用的。



一般来说小型企业的代码库并不会有很高的访问频率,哪怕proxy稍微慢一点,影响也不大。



Docker


如果使用docker版并使用了docker-compose,配置文件中的选项可以参考如下:


environment:
GITLAB_OMNIBUS_CONFIG: |
nginx['custom_gitlab_server_config'] = "location ~* ^/[^/]+/[^/]+$$ {\n if ($$http_user_agent ~* '^go.*') {\n return 200 \"<!DOCTYPE html><html><head><meta content='$$host$$uri git ssh://git@$$host:$$uri.git' name='go-import'></head></html>\";\n }\n proxy_cache off;\n proxy_pass http://gitlab-workhorse;\n}\nlocation ~* ^/(?<holder>[^/]+)/(?<project>[^/]+)/.*$$ {\n set $$goRedirect 'https://$$host/$$holder/$$project?$$args';\n if ($$http_user_agent ~* '^go.*') {\n return 301 $$goRedirect;\n }\n proxy_cache off;\n proxy_pass http://gitlab-workhorse;\n}"


使用\$\$可以防止参数被当成环境变量使用。



小结


代理的思维方式可以解决很多问题。


公众号广告=。=


原文链接
公众号二维码

Go 迷思之 Named 和 Unnamed Types

hxzqlh 发表了文章 • 0 个评论 • 200 次浏览 • 2017-11-28 09:54 • 来自相关话题

始发于微信公众号 查看全部

始发于微信公众号 Go 迷思之 Named 和 Unnamed Types


先来热身一下,下面的代码能编译吗?为什么?


package main

type stack []uintptr

func callers() stack {
return make([]uintptr, 20)
}

func main() {
callers()
}

(此处省略一分钟冥思苦想状....)



好啦,不用多想了,当然可以编译。


但是……这个问题重要吗?


是的,很重要。


如果上面这份代码不能编译,那意味着你无法写这样的代码:


type stack []uintptr
var st stack = make([]uintptr, 20)

而我们知道,这样的代码几乎无处不在。


再来,下面的代码能通过编译吗?


type T int

func F(t T) {}

func main() {
var q int
F(q)
}

结合你平时写的代码,再思考一分钟……


Ops, it couldn't。


稍微改动如下,它能通过编译吗?


type T []int

func F(t T) {}

func main() {
var q []int
F(q)
}

Yes, it does.


Surprised?! How could this happen?


Read The Fxxking Manual


言归正传,先来看下这又臭又长的 《Go 规范手册》 是怎么解释 Types 的。



A type determines a set of values together with operations and methods specific to those values. A type may be denoted by a type name, if it has one, or specified using a type literal, which composes a type from existing types.


Named instances of the boolean, numeric, and string types are predeclared. Other named types are introduced with type declarations. Composite types—array, struct, pointer, function, interface, slice, map, and channel types—may be constructed using type literals.


Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its type declaration.



Named vs Unnamed Type


Named types 有两类:



  • 内置的类型,比如 int, int64, float, string, bool,

  • 用 type 关键字声明的类型,比如 type Foo string


Unamed types:基于已有的 named types 声明出的组合类型,uname types 在 Go 里俯拾皆是。比如 struct{}、[]string、interface{}、map[string]bool、[20]float32……


Named types 可以作为方法的接受者, unnamed type 却不能。比如:


type Map map[string]string

// ok
func (m Map) Set(key string, value string){
m[key] = value
}

// invalid receiver type map[string]string (map[string]string is an unnamed type)
func (m map[string]string) Set(key string, value string){
m[key] = value
}

Underlying Type


每种类型 T 都有一个底层类型:如果 T 是预声明类型或者 类型字面量(笔者注:type literal 翻译成类型字面量,地道不?) ,它的底层类型就是 T 本身,否则,T 的底层类型是其类型声明中引用的类型的底层类型。


type (
B1 string
B2 B1
B3 []B1
B4 B3
)

string, B1 和 B2 的底层类型是 string.



B2 引用了 B1,那么 B2 的底层类型其实是 B1 的底层类型,而 B1 又引用了 string,那么 B1 的底层类型其实是 string 的底层类型,很明显,string 的底层类型就是string,最终 B2 的底层类型是 string。



[]B1, B3, 和 B4 的底层类型是 []B1.



[]B1 是类型字面量,因此它的底层类型就是它本身。



所有基于相同 unnamed types 声明的变量的类型都相同,而对于 named types 变量而言,即使它们的底层类型相同,它们也是不同类型。


// x 是 unnamed types
var x struct{ I int }

// x 和 x2 类型相同
var x2 struct{ I int }

// y 是 named type
type Foo struct{ I int }
var y Foo

// y 和 z 类型不同
type Bar struct{ I int }
var z Bar

Assignability


不同类型的变量之间是不能赋值的。


type MyInt int
var i int = 2
var i2 MyInt = 4
i = i2 // error: cannot use i2 (type MyInt) as type int in assignment

你不能把 i2 赋值给 i,因为它们的类型不同,虽然它们的底层类型都是 int。


对于那些拥有相同底层类型的变量而言,还需要理解另外一个重要概念:可赋值性。在 Assignability 的六大准则中,其中有一条:



x's type V and T have identical underlying types and at least one of V or T is not a defined type.



也就是说底层类型相同的两个变量可以赋值的条件是:至少有一个不是 named type。


x  = y   // ok
y = x // ok
x = x2 // ok
y = z // error: cannot use y (type Foo) as type Bar in assignment

现在,你知道“为什么开头那两份代码为什么一个能编译另一个不能”了吧。


Type Embedding


当你使用 type 声明了一个新类型,它不会继承原有类型的方法集。


package main

type User struct {
Name string
}

func (u *User) SetName(name string) {
u.Name = name
}

type Employee User

func main(){
employee := new(Employee)
employee.SetName("Jack").
// error employee.SetName undefined (type *Employee has no field or method SetName)
}

作为一个小技巧,你可以将原有类型作为一个匿名字段内嵌到 struct 当中来继承它的方法,这样的 struct 在 Go 代码中太常见不过了。


比如:


package main

type User struct {
Name string
}

func (u *User) SetName(name string) {
u.Name = name
}

type Employee struct {
User // annonymous field
Title string
}

func main(){
employee := new(Employee)
employee.SetName("Jack")
}

Last But Not Least


Go 里面关于类型 Types 的一些规定有时候让初学者丈二和尚摸不着头脑,而 Types 几乎是任何一门编程语言的基石,如果你不能理解 Go 里面最基本的概念之一:Types,相信我,你将不可能在这门语言上走远。


Teleport v2.5发布,支持限制包大小与自定义包协议

henrylee2cn 发表了文章 • 0 个评论 • 329 次浏览 • 2017-11-15 21:32 • 来自相关话题

Teleport v2.5(简称tp v2.5)今日发布啦!它是一个通用、高效、灵活的TCP Socket框架。可用于Peer-Peer对等通信、... 查看全部

Teleport v2.5(简称tp v2.5)今日发布啦!它是一个通用、高效、灵活的TCP Socket框架。可用于Peer-Peer对等通信、RPC、长连接网关、微服务、推送服务,游戏服务等领域。这次升级新增了自定义通信协议、包大小限制等一些新特性,并作了一系列深度优化。


tp v2.5 特性变化:



  • 【新增】支持设置读取包的大小限制(如果超出则断开连接)

  • 【新增】支持定制通信协议

  • 【升级】支持插件机制,可以自定义认证、心跳、微服务注册中心、统计信息插件等

  • 【优化】无论服务器或客户端,均支持优雅重启、优雅关闭

  • 支持实现反向代理功能

  • 【优化】日志信息详尽,支持打印输入、输出消息的详细信息(状态码、消息头、消息体)

  • 服务器和客户端之间对等通信,两者API方法基本一致

  • 底层通信数据包包含HeaderBody两部分

  • 支持单独定制HeaderBody编码类型,例如JSON Protobuf string

  • Body支持gzip压缩

  • Header包含状态码及其描述文本

  • 支持推、拉、回复等通信模式

  • 支持设置慢操作报警阈值

  • 底层连接使用I/O缓冲区

  • 端点间通信使用I/O多路复用技术


teleport




tp v2.5 升级详情:


一、增加对自定义通信协议的支持,通过实现socket.Protocol接口来定制:


// Protocol socket communication protocol
type Protocol interface {
// WritePacket writes header and body to the connection.
WritePacket(
packet *Packet,
destWriter *utils.BufioWriter,
tmpCodecWriterGetter func(string) (*TmpCodecWriter, error),
isActiveClosed func() bool,
) error

// ReadPacket reads header and body from the connection.
ReadPacket(
packet *Packet,
bodyAdapter func() interface{},
srcReader *utils.BufioReader,
codecReaderGetter func(byte) (*CodecReader, error),
isActiveClosed func() bool,
checkReadLimit func(int64) error,
) error
}

然后,可以通过以下任意方法指定自己的通信协议:


func SetDefaultProtocol(socket.Protocol)
func (*Peer) ServeConn(conn net.Conn, protocol ...socket.Protocol) Session
func (*Peer) DialContext(ctx context.Context, addr string, protocol ...socket.Protocol) (Session, error)
func (*Peer) Dial(addr string, protocol ...socket.Protocol) (Session, error)
func (*Peer) Listen(protocol ...socket.Protocol) error

二、新增限制通信包大小


在读取包时可以限制包的大小,如果超出最大值则会主动断开连接。全局设置函数:


func SetReadLimit(maxPacketSize int64)

三、升级插件接口



  1. 插件返回值由以前的error改为tp.Xerror,从而用户可以灵活地在插件中定义错误码和错误描述;

  2. 增加更多、更细、更合理的插件位置

  3. 插件执行出错时的日志格式更加清晰整洁


// Interfaces about plugin.
type (
Plugin interface {
Name() string
}
PostRegPlugin interface {
Plugin
PostReg(*Handler) Xerror
}
PostDialPlugin interface {
Plugin
PostDial(PreSession) Xerror
}
PostAcceptPlugin interface {
Plugin
PostAccept(PreSession) Xerror
}
PreWritePullPlugin interface {
Plugin
PreWritePull(WriteCtx) Xerror
}
PostWritePullPlugin interface {
Plugin
PostWritePull(WriteCtx) Xerror
}
PreWriteReplyPlugin interface {
Plugin
PreWriteReply(WriteCtx) Xerror
}
PostWriteReplyPlugin interface {
Plugin
PostWriteReply(WriteCtx) Xerror
}
PreWritePushPlugin interface {
Plugin
PreWritePush(WriteCtx) Xerror
}
PostWritePushPlugin interface {
Plugin
PostWritePush(WriteCtx) Xerror
}
PreReadHeaderPlugin interface {
Plugin
PreReadHeader(ReadCtx) Xerror
}

PostReadPullHeaderPlugin interface {
Plugin
PostReadPullHeader(ReadCtx) Xerror
}
PreReadPullBodyPlugin interface {
Plugin
PreReadPullBody(ReadCtx) Xerror
}
PostReadPullBodyPlugin interface {
Plugin
PostReadPullBody(ReadCtx) Xerror
}

PostReadPushHeaderPlugin interface {
Plugin
PostReadPushHeader(ReadCtx) Xerror
}
PreReadPushBodyPlugin interface {
Plugin
PreReadPushBody(ReadCtx) Xerror
}
PostReadPushBodyPlugin interface {
Plugin
PostReadPushBody(ReadCtx) Xerror
}

PostReadReplyHeaderPlugin interface {
Plugin
PostReadReplyHeader(ReadCtx) Xerror
}
PreReadReplyBodyPlugin interface {
Plugin
PreReadReplyBody(ReadCtx) Xerror
}
PostReadReplyBodyPlugin interface {
Plugin
PostReadReplyBody(ReadCtx) Xerror
}

PostDisconnectPlugin interface {
Plugin
PostDisconnect(PostSession) Xerror
}
)

四、更多细节优化



  1. 运行日志中打印增加包序号seq,便于debug

  2. 当收到不支持的包类型时,断开连接并打印包详情

  3. tp.PullCmd增加func (c *PullCmd) Result() (interface{}, Xerror)方法,便于使用Session.GoPull方法进行并发请求

  4. 升级平滑重启与关闭功能

  5. 增加对并发资源的控制,防止内存资源耗尽

  6. 一些代码块的细节优化


Teleport项目地址:
https://github.com/henrylee2cn/teleport

结合 Go 读 APUE-文件共享

zhaohu 发表了文章 • 0 个评论 • 230 次浏览 • 2017-11-10 21:26 • 来自相关话题

在公众号 "别捉急" 上 同步了文章,并且可以点击原文链接阅读:查看全部


在公众号 "别捉急" 上 同步了文章,并且可以点击原文链接阅读:传送门



文件共享


UNIX 系统支持在不同进程间共享打开文件, 知识点:内核用于所有 I/O 的数据结构、原子操作。


概念性的 I/O 数据结构


内核用于所有 I/O 的数据结构,只是个概念性的,不一定适用,有个大体的轮廓就 OK。



  • 进程表 (process table entry) 中的记录

  • 文件表项 (file table entry)

  • v节点表项 (v-node table entry)


打开文件的内核数据结构


这是一个 打开文件的内核数据结构 图。打开文件 这个操作是一个进程, 每个进程在进程表中都有一个记录,而 打开文件进程记录 中包含一张打开文件描述符表, 包括:



  • 文件描述符标志

  • 指向一个文件表项的指针


文件描述符表用 Go 抽象如下表示:


type fd struct {
flags int
pointer *FileTableEntry
}

代码中 flags 的类型是随便定义的(实际我没查),由图中看出 pointer 指向文件表项 (file table entry), 内核为所有打开文件维持一张文件表, 每个文件表项包括:



  • 文件状态标志 (读、写、添写、同步和非阻塞)

  • 当前文件偏移量

  • 指向该文件 v 节点表项的指针


文件表项用 Go 抽象如下表示:


type FileTableEntry struct {
status int
offset int
pointer *VNodeTableEntry
}

由图中看出 pointer 指向v节点表项 (v-node table entry), 每个打开文件都有一个 v 节点结构如下所示:



  • 文件类型和对此文件进行各种操作函数的指针,统称为 v节点信息

  • 该文件的 i 节点: 文件所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等


V 节点表项和 i 节点用 Go 抽象如下:


type VNodeTableEntry struct {
information *Information
vData *INode
}

type INode struct {
owner *Owner
length int
vNodeTableEntry *VNodeTableEntry
}

通过这种方式,来加深对 内核通用 I/O 数据结构 的理解。


如果两个独立进程各自打开同一个文件,则三者关系如下所示:


两个独立进程打开同一个文件


原子操作


一般而言,原子操作 (atomic operation) 指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。


函数 dup 和 dup2


下面两个函数都可用来复制一个现有的文件描述符。


#include <unistd.h>

int dup(int fd);

int dup2(int fd, int fd2);

上面函数中的参数:



  • fd 表示要复制的文件描述符

  • fd2 表示复制后的文件描述符


dup2 函数是可以指定复制后的文件描述符,而 dup 是返回当前可用文件描述符中的最小数值。


调用 dup(1) 函数后,进程表项,文件表,v 节点表,之间的关系图如下:
dup(1) 后的内核数据结构


对于传入的参数 fd2, 已经被打开了,会先关闭。知道了这个点,就明白了,下面的操作中会先调用 close(fd2)


很不爽了,没找到相应的 Go 的源码。复制一个描述符的另一种方法是使用 fcntl函数, dup2(fd, fd2) 等效于 close(fd2)fcntl(fd, F_DUPFD, fd2), 但不完全等效,因为 dup2(fd, fd2) 是个原子操作。


目前我先知道 fcntl函数 可以改变已经打开文件的属性,就可以啦。

不再傻傻分不清:atoi, itoa, iota

Xanthus 发表了文章 • 0 个评论 • 242 次浏览 • 2017-11-09 21:01 • 来自相关话题

atoi

Array to Integer 字符数组(字符串)转化为整数。golang标准库与C++标准库均有

itoa

Integer to Array 整数转化为字符串。golang标准库与C++标准... 查看全部

atoi


Array to Integer
字符数组(字符串)转化为整数。golang标准库与C++标准库均有


itoa


Integer to Array
整数转化为字符串。golang标准库与C++标准库均有


iota


希腊字母。golang中定义常量经常用的iota关键字,C艹中用于Store increasing sequence

emmmmm, 都是递增

Go并发机制

simple 发表了文章 • 0 个评论 • 619 次浏览 • 2017-10-08 22:31 • 来自相关话题

不知道为什么有三张图在这里无法显示,有点郁闷!!! ->查看全部

不知道为什么有三张图在这里无法显示,有点郁闷!!! ->原文链接


1. C/C++ 与 Go语言的“价值观”对照


之前看过 白明老师 在GopherChina2017的一篇演讲文章《Go coding in go way》,里面提到C/C++/Go三门语言价值观,感觉很有意思,分享给大家感受一下:


C的价值观摘录



  • 相信程序员:提供指针和指针运算,让C程序员天马行空的发挥

  • 自己动手,丰衣足食:提供一个很小的标准库,其余的让程序员自造

  • 保持语言的短小和简单

  • 性能优先


C++价值观摘录



  • 支持多范式,不强迫程序员使用某个特定的范式

  • 不求完美,但求实用(并且立即可用)


Go价值观



  • Overall Simplicity 全面的简单

  • Orthogonal Composition 正交组合

  • Preference in Concurrency 偏好并发


用一句话概括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的话,剩下的所有对应的用户线程全都会变为阻塞状态(整个进程挂起)。

所以这些语言的协程库会把自己一些阻塞的操作重新封装为完全的非阻塞形式,然后在以前要阻塞的点上,主动让出自己,并通过某种方式通知或唤醒其他待执行的用户线程在该KSE上运行,从而避免了内核调度器由于KSE阻塞而做上下文切换,这样整个进程也不会被阻塞了。


2.3 混合型线程模型


用户线程与KSE是多对多关系(M:N), 这种实现综合了前两种模型的优点,为一个进程中创建多个KSE,并且线程可以与不同的KSE在运行时进行动态关联,当某个KSE由于其上工作的线程的阻塞操作被内核调度出CPU时,当前与其关联的其余用户线程可以重新与其他KSE建立关联关系。当然这种动态关联机制的实现很复杂,也需要用户自己去实现,这算是它的一个缺点吧。Go语言中的并发就是使用的这种实现方式,Go为了实现该模型自己实现了一个运行时调度器来负责Go中的"线程"与KSE的动态关联。此模型有时也被称为 两级线程模型即用户调度器实现用户线程到KSE的“调度”,内核调度器实现KSE到CPU上的调度


三种模型的示意图如下:
图片1


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的并发用起来非常简单,用了一个语法糖将内部复杂的实现结结实实的包装了起来。其内部可以用下面这张图来概述:
图片2
其图中的G, P和M都是Go语言运行时系统(其中包括内存分配器,并发调度器,垃圾收集器等组件,可以想象为Java中的JVM)抽象出来概念和数据结构对象:

G:Goroutine的简称,上面用go关键字加函数调用的代码就是创建了一个G对象,是对一个要并发执行的任务的封装,也可以称作用户态线程。属于用户级资源,对OS透明,具备轻量级,可以大量创建,上下文切换成本低等特点。

M:Machine的简称,在linux平台上是用clone系统调用创建的,其与用linux pthread库创建出来的线程本质上是一样的,都是利用系统调用创建出来的OS线程实体。M的作用就是执行G中包装的并发任务。Go运行时系统中的调度器的主要职责就是将G公平合理的安排到多个M上去执行。其属于OS资源,可创建的数量上也受限了OS,通常情况下G的数量都多于活跃的M的。

P:Processor的简称,逻辑处理器,主要作用是管理G对象(每个P都有一个G队列),并为G在M上的运行提供本地化资源。

从2.3节介绍的两级线程模型来看,似乎并不需要P的参与,有G和M就可以了,那为什么要加入P这个东东呢?

其实Go语言运行时系统早期(Go1.0)的实现中并没有P的概念,Go中的调度器直接将G分配到合适的M上运行。但这样带来了很多问题,例如,不同的G在不同的M上并发运行时可能都需向系统申请资源(如堆内存),由于资源是全局的,将会由于资源竞争造成很多系统性能损耗,为了解决类似的问题,后面的Go(Go1.1)运行时系统加入了P,让P去管理G对象,M要想运行G必须先与一个P绑定,然后才能运行该P管理的G。这样带来的好处是,我们可以在P对象中预先申请一些系统资源(本地资源),G需要的时候先向自己的本地P申请(无需锁保护),如果不够用或没有再向全局申请,而且从全局拿的时候会多拿一部分,以供后面高效的使用。就像现在我们去政府办事情一样,先去本地政府看能搞定不,如果搞不定再去中央,从而提供办事效率。

而且由于P解耦了G和M对象,这样即使M由于被其上正在运行的G阻塞住,其余与该M关联的G也可以随着P一起迁移到别的活跃的M上继续运行,从而让G总能及时找到M并运行自己,从而提高系统的并发能力。

Go运行时系统通过构造G-P-M对象模型实现了一套用户态的并发调度系统,可以自己管理和调度自己的并发任务,所以可以说Go语言原生支持并发自己实现的调度器负责将并发任务分配到不同的内核线程上运行,然后内核调度器接管内核线程在CPU上的执行与调度。


3.2 调度过程


Go运行时完整的调度系统是很复杂,很难用一篇文章描述的清楚,这里只能从宏观上介绍一下,让大家有个整体的认识。


// Goroutine1
func task1() {
go task2()
go task3()
}

假如我们有一个G(Goroutine1)已经通过P被安排到了一个M上正在执行,在Goroutine1执行的过程中我们又创建两个G,这两个G会被马上放入与Goroutine1相同的P的本地G任务队列中,排队等待与该P绑定的M的执行,这是最基本的结构,很好理解。 关键问题是:

a.如何在一个多核心系统上尽量合理分配G到多个M上运行,充分利用多核,提高并发能力呢?
如果我们在一个Goroutine中通过go关键字创建了大量G,这些G虽然暂时会被放在同一个队列, 但如果这时还有空闲P(系统内P的数量默认等于系统cpu核心数),Go运行时系统始终能保证至少有一个(通常也只有一个)活跃的M与空闲P绑定去各种G队列去寻找可运行的G任务,该种M称为自旋的M。一般寻找顺序为:自己绑定的P的队列,全局队列,然后其他P队列。如果自己P队列找到就拿出来开始运行,否则去全局队列看看,由于全局队列需要锁保护,如果里面有很多任务,会转移一批到本地P队列中,避免每次都去竞争锁。如果全局队列还是没有,就要开始玩狠的了,直接从其他P队列偷任务了(偷一半任务回来)。这样就保证了在还有可运行的G任务的情况下,总有与CPU核心数相等的M+P组合 在执行G任务或在执行G的路上(寻找G任务)。

b. 如果某个M在执行G的过程中被G中的系统调用阻塞了,怎么办?

在这种情况下,这个M将会被内核调度器调度出CPU并处于阻塞状态,与该M关联的其他G就没有办法继续执行了,但Go运行时系统的一个监控线程(sysmon线程)能探测到这样的M,并把与该M绑定的P剥离,寻找其他空闲或新建M接管该P,然后继续运行其中的G,大致过程如下图所示。然后等到该M从阻塞状态恢复,需要重新找一个空闲P来继续执行原来的G,如果这时系统正好没有空闲的P,就把原来的G放到全局队列当中,等待其他M+P组合发掘并执行。

图片3

c. 如果某一个G在M运行时间过长,有没有办法做抢占式调度,让该M上的其他G获得一定的运行时间,以保证调度系统的公平性?

我们知道linux的内核调度器主要是基于时间片和优先级做调度的。对于相同优先级的线程,内核调度器会尽量保证每个线程都能获得一定的执行时间。为了防止有些线程"饿死"的情况,内核调度器会发起抢占式调度将长期运行的线程中断并让出CPU资源,让其他线程获得执行机会。当然在Go的运行时调度器中也有类似的抢占机制,但并不能保证抢占能成功,因为Go运行时系统并没有内核调度器的中断能力,它只能通过向运行时间过长的G中设置抢占flag的方法温柔的让运行的G自己主动让出M的执行权。

说到这里就不得不提一下Goroutine在运行过程中可以动态扩展自己线程栈的能力,可以从初始的2KB大小扩展到最大1G(64bit系统上),因此在每次调用函数之前需要先计算该函数调用需要的栈空间大小,然后按需扩展(超过最大值将导致运行时异常)。Go抢占式调度的机制就是利用在判断要不要扩栈的时候顺便查看以下自己的抢占flag,决定是否继续执行,还是让出自己。

运行时系统的监控线程会计时并设置抢占flag到运行时间过长的G,然后G在有函数调用的时候会检查该抢占flag,如果已设置就将自己放入全局队列,这样该M上关联的其他G就有机会执行了。但如果正在执行的G是个很耗时的操作且没有任何函数调用(如只是for循环中的计算操作),即使抢占flag已经被设置,该G还是将一直霸占着当前M直到执行完自己的任务。


4. Goroutine与Channel: 锁之外的另一种同步机制


在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。

除了这些基本的同步手段,Go语言还提供了一种新的同步机制: Channel,它在Go语言中是一个像int, float32等的基本类型,一个channel可以认为是一个能够在多个Goroutine之间传递某一类型的数据的管道。Go中的channel无论是实现机制还是使用场景都和Java中的BlockingQueue很接近。


使用方式


// 声明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();

使用场景

a. 与Java的BlockingQueue一样用在需要生产者消费者模型的并发环境中。

b. 锁同步场景下一种替代方案。在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信,而要以通信的方式去共享内存。在Go语言中并不鼓励用锁保护共享状态的方式在不同的Goroutine中分享信息(以共享内存的方式去通信)。而是鼓励通过channel将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存),这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。但这的确需要转换以前用锁做并发同步的思维方式,大家觉得那种适合自己和自己的使用场景就用哪种好了,并不能很简单、绝对地说哪种方式更好,更高效。


5. Go语言对网络IO的优化


在谈论高性能网络IO编程时,我们几乎都离不开epoll/kqueue/iocp等技术术语了,如Java最新的的NIO,Node等等的高性能网络IO模型都是基于这些技术实现的。诞生于21世纪,有互联网时代的C语言之称的Go语言,这么重视高并发,当然不会放过对网络的优化。且Go语言中对网络IO的优化很巧妙,让你可以用和以前一样的(同步的)思维方式去编程的同时(而不是反人类的异步方式),还能享受到与异步方式几乎同等高效的运行性能。那Go语言中是如何做的呢?主要是从两方面下手的:

a. 将标准库中的网络库全部封装为非阻塞形式,防止其阻塞底层的M并导致内核调度器切换上下文带来的系统开销。

b. 运行时系统加入epoll机制(针对Linux系统),当某一个Goroutine在进行网络IO操作时,如果网络IO未就绪,就将其该Goroutine封装一下,放入epoll的等待队列中,当前G挂起,与其关联的M可以继续运行其他G。当相应的网络IO就绪后,Go运行时系统会将等待网络IO就绪的G从epoll就绪队列中取出(主要在两个地方从epoll中获取已网络IO就绪的G列表,一是sysmon监控线程中,二是自旋的M中),再由调度器将它们像普通的G一样分配给各个M去执行。

Go语言将高性能网络IO的实现方式直接集成到了Go本身的运行时系统中,与Go的并发调度系统协同高效的工作,让开发人员可以简单,高效地进行网络编程。


6. 总结


Go语言并不完美,它是以软件工程为目的的语言设计。其实现的并发机制也并不是什么革新的技术,只是将这些经典的理论和技术以一种简洁高效的方式组合了起来,并用简单抽象的API或语法糖开放给开发人员,着实减轻了开发人员编程的心智负担。而且其通过引入channel机制,将另一种并发编程模型(CSP: 通信顺序进程)带给了我们,给我们提供了使用其他并发编程思维方式的机会(有关CSP模型建议大家看看《七周七并发模型》这本书的第六章),Goroutine与Channel的组合是一对很有powerful的并发工具,相信其可以给你带了更好的并发编程体验。


7. 参考


《Go并发编程实战》 第2版

《Go语言学习笔记》

也谈goroutine调度器

Go coding in go way

一个多功能心跳发送包——yapool

千手扉间 发表了文章 • 0 个评论 • 333 次浏览 • 2017-09-14 19:42 • 来自相关话题

自己实现了一个多功能心跳包

传送门 https://github.com/CrocdileChan/yapool

因为之... 查看全部

自己实现了一个多功能心跳包


传送门 https://github.com/CrocdileChan/yapool


因为之前的项目需要,我将一部分功能逻辑抽象出来,这个包可以供给做分布式的小伙伴用来造轮子。


基于这个包,可以轻易的实现服务发现、健康监测以及集群数据采集功能,心跳可以分为多个等级,开发者可以在里面定义自己需要传送到center(或者叫master)的讯息,center端可以对该信息进行处理。

go 1.9 多线程安全MAP 函数模块

alalmn 发表了文章 • 0 个评论 • 701 次浏览 • 2017-08-28 16:02 • 来自相关话题

package main

//go 1.9  多线程安全MAP  函数模块
//QQ:29295842  欢迎技术交流
import (
    //  "fmt"
    "s... 			查看全部
					
package main

//go 1.9 多线程安全MAP 函数模块
//QQ:29295842 欢迎技术交流
import (
// "fmt"
"sync"
)

var (
map_list sync.Map //广告配置信息
wgx sync.WaitGroup //
)

func Thread_map_add(id string, rows_map map[string]interface{}) { //添加数据
map_list.Store(id, rows_map)
}

func Thread_map_revise(id string, rows_map map[string]interface{}) { //修改
wgx.Add(1) //线程数
go func() {
map_list.LoadOrStore(id, rows_map) //修改
wgx.Done()
}()
wgx.Wait() //等待
}

func Thread_map_delete(id string) { //删除
wgx.Add(1) //线程数
go func() {
map_list.Delete(id) //删除
wgx.Done()
}()
wgx.Wait() //等待
}

func Thread_map_read(id string) (bool, string, map[string]interface{}) { //读取
read_bool := false
value := make(map[string]interface{})
value2, err := map_list.Load(id) //key读取
if err {
if valuexa, ok := value2.(map[string]interface{}); ok {
value = valuexa
read_bool = true
}
}
//fmt.Print("===%v==%v==\n", data, err)
//遍历读取
// map_list.Range(func(key, value2 interface{}) bool { //读取数据
// fmt.Println(key, "-----------", value2)
// // if valuexa, ok := value2.(map[string]interface{}); ok {
// // read_bool = true
// // //key = fmt.Sprintf("%v", key)
// // value = valuexa
// // }
// return true
// })
return read_bool, id, value
}

//fmt.Println("----------------")

// rows_map := make(map[string]interface{})
// rows_map["db_name"] = "098765"
// rows_map["list"] = "1234567"

// map_add("abc", rows_map)
// rows_map = make(map[string]interface{})
// rows_map["db_name"] = "aaaaaa"
// rows_map["list"] = "bbbbbbb"
// //map_add("123", rows_map)
// map_revise("abc", rows_map)
// rows_map3 := make(map[string]interface{})
// rows_map3["db_name"] = "rrrrr"
// rows_map3["list"] = "eeeeeee"
// map_add("1234", rows_map3)

// read_bool, key, re_map := map_read("abc")
// fmt.Printf("==%v==%v==%v==\n", read_bool, key, re_map["db_name"])

redis 分布式锁

toukii 发表了文章 • 0 个评论 • 419 次浏览 • 2017-08-26 12:48 • 来自相关话题

rddlock

github.com/everfore/rddlock

redis distribute lock

... 查看全部

rddlock


github.com/everfore/rddlock


redis distribute lock


redis 分布式锁, 实现原理:redis分布式锁


Usage


Lock & UnLock


lockkey := "lock-key"
timeout_ms := 3000

locked, ex := rddlock.Lock(rds, lockkey, timeout_ms)
defer reelock.UnLock(rds, lockkey, ex)

LockRetry


retry_times := 10
locked, ex := reelock.LockRetry(rds, lockkey, timeout_ms, retry_times) // get lock by retry
defer reelock.UnLock(rds, lockkey, ex)

UnLockUnsafe


直接删除key,可能会有问题:若删除之前,该key已经超时且被其他进程获得锁,将会删除其他进程的锁;删除之后,锁被释放,进而会有其他进程2获得锁。。。雪崩


locked, _ := rddlock.Lock(rds, lockkey, timeout_ms)
defer reelock.UnLockUnsafe(rds, lockkey)

SyncDo 异步执行任务


err := SyncDo(rds, lockkey, timeout_ms, func(timeout chan bool) chan bool {
ret := make(chan bool, 1)
go func() {
fmt.Println("doing...")
// TODO SOMETHING
select {
case <-timeout:
// do the rollback
break
case ret <- true:
fmt.Println("success end.")
}
}()
return ret
})

test


success:200, avg:1.1074123 ms
failed:0, avg:NaN ms
--- PASS: TestLockTime (10.59s)

#local-redis
=== RUN TestLockRetryTime
success:200, avg:1.1741205 ms
failed:0, avg:NaN ms
--- PASS: TestLockRetryTime (10.58s)

#uat-redis
=== RUN TestLockRetryTime
success:200, avg:12.572702 ms
failed:0, avg:NaN ms
--- PASS: TestLockRetryTime (10.59s)

欢迎指正 github.com/everfore/rddlock

PHP编码gzdeflate与Golang解码DEFLATE

qiangmzsx 发表了文章 • 0 个评论 • 257 次浏览 • 2017-08-25 00:00 • 来自相关话题

8月7日@黄同学找我问:“数据存到redis是gzdeflate压缩过的数据,golang从redis取出来,解压缩失败”。很多从PHP转Golang的业务经常会遇到,所以写下这篇博文,希望可以帮助更多人。
想要使用golang解码php的编... 查看全部

8月7日@黄同学找我问:“数据存到redis是gzdeflate压缩过的数据,golang从redis取出来,解压缩失败”。很多从PHP转Golang的业务经常会遇到,所以写下这篇博文,希望可以帮助更多人。

想要使用golang解码php的编码,那么就应该需要知道gzdeflate函数的算法是什么,先到gzdeflate文档,查看了一下发现:

gzdeflate使用的是纯粹的DEFLATE格式。这就与golang的compress/flate包一致了。有了了解就可以看着golang文档实现代码了。遂与@黄同学同学写了几个函数进行验证,最后定稿如下:


package main

import (
"strings"
"fmt"
"compress/flate"
"bytes"
"io/ioutil"
"github.com/bitly/go-simplejson"
)

func main() {

str:="test123"
b:=Gzdeflate(str,-1)
ss:=Gzdecode(string(b))
fmt.Println(ss)
}

// 解码
func Gzdecode(data string) string {
if data == "" {
return ""
}
r :=flate.NewReader(strings.NewReader(data))
defer r.Close()
out, err := ioutil.ReadAll(r)
if err !=nil {
fmt.Errorf("%s\n",err)
return ""
}
return string(out)
}

// 编码
func Gzdeflate(data string,level int) []byte {
if data == "" {
return []byte{}
}
var bufs bytes.Buffer
w,_ :=flate.NewWriter(&bufs,level)
w.Write([]byte(data))
w.Flush()
defer w.Close()
return bufs.Bytes()
}

// 编码
func GzdeflateForString(data string,level int) string {
if data == "" {
return ""
}
var bufs bytes.Buffer
w,_ :=flate.NewWriter(&bufs,level)
w.Write([]byte(data))
w.Flush()
defer w.Close()
return bufs.String()
}

经过@黄同学同学测试可以正确使用。留下wiki供后续遇到的同学查看。

Golang Label使用方法

haohongfan 发表了文章 • 0 个评论 • 379 次浏览 • 2017-08-22 10:00 • 来自相关话题

发现golang的label也是比较强大的, 这里看了一些资料, 搜集了一些特点, 欢迎指正 http://www.haoho...

[译]go styleguide

回复

Xargin 发起了问题 • 1 人关注 • 0 个回复 • 638 次浏览 • 2017-08-04 17:39 • 来自相关话题

NoPoint Docker — 云时代的程序分发方式

Julyqi 发表了文章 • 0 个评论 • 349 次浏览 • 2017-07-28 16:17 • 来自相关话题

要说最近一年云计算业界有什么大事件?Google Compute Engine 的正式发布?Azure入华?还是AWS落地中国?留在每个人大脑中的印象可能各不相同,但要是让笔者来排名的话那么Docker绝对应该算是第一位的。如果你之前听说过它的话,那么也... 查看全部

要说最近一年云计算业界有什么大事件?Google Compute Engine 的正式发布?Azure入华?还是AWS落地中国?留在每个人大脑中的印象可能各不相同,但要是让笔者来排名的话那么Docker绝对应该算是第一位的。如果你之前听说过它的话,那么也许你会说“没错,就是它”,因为几乎世界各地的开发、运维都在谈论着Docker;如果你还没听说过Docker,那么我真的建议你花上5分钟来阅读本文。


https://community.clouderwork.com/article/view/59633469d009d.html


总之笔者认为Docker还是非常有趣的一个东西,值得大家花些时间体验一下,相信在各位的工作中多多少少都能用的上Docker。

GOLANG中DEFER, PANIC, RECOVER用法

回复

haohongfan 发起了问题 • 1 人关注 • 0 个回复 • 572 次浏览 • 2017-07-21 11:37 • 来自相关话题