使用了Go mod管理的项目里如何添加新的第三方包?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 192 次浏览 • 2020-09-17 11:32 • 来自相关话题

如何快速提升Go程序性能?

zkbhj 发表了文章 • 0 个评论 • 97 次浏览 • 2020-09-15 18:39 • 来自相关话题

这篇文章中列出了一些不需要太多精力就能显著提高性能的技巧,并不包含那些需要太多精力或需要大幅度修改程序结构的技巧。
 
开始优化之前
 
开始优化之前,首先应该花些时间找出一个合适的基准线,以便稍后比较。如果没有基准,那就等于摸着石头过河,根本不知道自己的优化有没有效果。首先要编写性能测试程序,然后生成能用于pprof的profile文件。最好可以编写Go的性能测试脚本(https://dave.cheney.net/2013/0 ... in-go),这样可以很容易地使用pprof,还可以评测内存分配情况。还可以使用benchcmp,这个工具可以帮助比较两次性能测试之间的性能差异。

如果代码很难做性能测试,那就从你能测量时间的部分开始。可以利用runtime/pprof手工测量代码。

现在开始吧!
 
使用sync.Pool重用之前分配过的对象
 
sync.Pool实现了一个空闲列表(free-list)。这样可以重新使用之前分配过的对象。这样做可以将对象分配的代价平摊到多次使用上,减少垃圾回收器的工作。API非常简单:只需实现一个函数,用来分配新的对象即可。它会返回指针类型。
 
var bufpool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 512)
return &buf
}}之后,可以用Get()从池中获取对象,用完之后用Put()将对象放回。

不过要注意一些陷阱。在Go 1.13之前,每次发生垃圾回收时该池都会被清空。对于需要分配大量对象的程序来说,这可能会造成性能的影响。在1.13版本中似乎GC后能保留更多对象了(https://go-review.googlesource.com/c/go/+/162919/)。

你可能需要在将对象放回池中之前将其结构的字段清空。

如果不这样做,就可能从池中获得一个“脏”的对象,它包含之前使用过的数据。这可能会造成严重的安全问题!
 
安全的做法就是明确清空内存:// reset resets all fields of the AuthenticationResponse before pooling it.
func (a* AuthenticationResponse) reset() {
a.Token = ""
a.UserID = ""
}

rsp := authPool.Get().(*AuthenticationResponse)
defer func() {
rsp.reset()
authPool.Put(rsp)
}()
在大map中避免使用包含指针的结构作为map的键
 
关于Go中大型堆的性能问题已经有很多人讨论过了。在垃圾回收过程中,运行时会扫描包含指针的对象并遍历其指针。如果你有非常大的map[string]int,那么垃圾回收器就不得不在每次垃圾回收过程中检查map中的每个字符串,因为字符串包含指针。

这个例子中我们向一个map[string]int中写入了一千万个元素,然后测量垃圾回收的时间。map是在包的作用域中分配的,以保证它被分配到堆上。
import (
"fmt"
"runtime"
"strconv"
"time"
)

const (
numElements = 10000000
)

var foo = map[string]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[strconv.Itoa(i)] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}运行后可以得到以下结果:gc took: 98.726321ms
gc took: 105.524633ms
gc took: 102.829451ms
gc took: 102.71908ms
gc took: 103.084104ms
gc took: 104.821989ms对于计算机来说这花得时间太多了!

怎样可以改进呢?最好是能尽量去掉指针,这样能减少垃圾回收器需要遍历的指针数量。由于字符串包含指针,因此我们可以用map[int]int来实现:
 
import (
"fmt"
"runtime"
"time"
)

const (
numElements = 10000000
)

var foo = map[int]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[i] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}重新运行程序,结果如下:
 
gc took: 3.608993ms
gc took: 3.926913ms
gc took: 3.955706ms
gc took: 4.063795ms
gc took: 3.91519ms
gc took: 3.75226ms好多了。垃圾回收的时间减少了97%。在生产环境下,字符串需要进行hash之后再插入到map中。
 
生成marshalling代码以避免运行时反射
 
将数据结构marshalh或unmarshal成JSON等各种序列化格式是个很常见的操作,特别是在构建微服务的时候。实际上,大部分微服务做的唯一工作就是序列化。像json.Marshal和json.Unmarshal需要依赖运行时反射才能将结构体的字段序列化成字节,反之亦然。这个操作很慢,反射的性能完全无法与显式的代码相比。

但我们不必这么做。marshalling JSON的原理大致如下:
package json

// Marshal take an object and returns its representation in JSON.
func Marshal(obj interface{}) ([]byte, error) {
// Check if this object knows how to marshal itself to JSON
// by satisfying the Marshaller interface.
if m, is := obj.(json.Marshaller); is {
return m.MarshalJSON()
}

// It doesn't know how to marshal itself. Do default reflection based marshallling.
return marshal(obj)
}如果我们知道怎样将对象marshal成JSON,就应该避免运行时反射。但我们不想手工marshal所有代码,怎么办呢?可以让计算机替我们写程序!像easyjson等代码生成器会检查结构体,然后生成高度优化且与json.Marshaller等接口完全兼容的代码。

下载这个包,然后在包含结构体的$file.go上运行下面的命令:
easyjson -all $file.go$file.go
这个命令会生成$file_easyjson.go。由于easyjson为我们实现了json.Marshaller接口,因此序列化时不会调用默认的反射,而是会使用生成的函数。祝贺你!你已经将JSON marshalling的代码的速度提高了三倍。还有许多其他技巧可以进一步提升性能。
 
使用strings.Builder来构建字符串
 
Go语言的字符串是不可修改的,可以认为它们是只读的字节切片。这就是说,每次创建字符串都要分配新的内存,可能还会给垃圾回收器造成更多工作。

Go 1.10引入了strings.Builder作为高效率构建字符串的方式。它内部会将字符串写入到字节缓冲区。只有在builder上调用String()时才会真正生成字符串。它依赖一些unsafe的技巧将底层的字节作为字符串返回,而不实际进行内存非配。这篇文章(https://syslog.ravelin.com/byt ... ca7ff)介绍了更多其工作原理。pkg: github.com/sjwhitworth/perfblog/strbuild
BenchmarkStringBuildNaive-8 5000000 255 ns/op 216 B/op 8 allocs/op
BenchmarkStringBuildBuilder-8 20000000 54.9 ns/op 64 B/op 1 allocs/op可见,strings.Builder要快4.7倍,它的内存分配次只有前者的1/8,内存使用只有前者的1/4。

所以,在性能重要的时候应该使用strings.Builder。一般来说,除非是非常不重要的情况,否则我建议永远使用strings.Builder来构建字符串。
 
使用strconv代替fmt

fmt是Go中最著名的包之一。估计你从第一个Go程序——输出“hello, world”——的时候就开始用它了。但是,如果需要将整数和浮点数转换成字符串,它就不如更底层的strconv有效率了。strconv只需要在API中进行很小改动,就能带来不错的性能提升。

大多数情况下fmt会接受一个interface{}作为参数。这样做有两个弊端:

失去了类型安全。对于我来说这是个很大的问题。

会增加内存分配次数。将非指针类型作为interface{}传递通常会导致堆分配的问题。进一步的内容可以阅读这篇文章(https://www.darkcoding.net/sof ... face/)。
BenchmarkStrconv-8 30000000 39.5 ns/op 32 B/op 1 allocs/op
BenchmarkFmt-8 10000000 143 ns/op 72 B/op 3 allocs/op可以看到,strconv版本要快3.5倍,内存分配次数是1/3,内存分配量是1/2。

在make中指定分配的容量来避免重新分配

在讨论性能改善之前,我们先来迅速看一下切片。切片是Go语言中一个非常有用的概念。它提供了可改变大小的数组,还可以用不同的方式表示同一片底层内存区域,而不需要重新进行内存分配。slice的内部结构由三个元素组成:

data:切片中指向底层数据的指针
len:切片中的当前元素数目
cap:在不重新分配内存的前提下,切片能够增长到的元素数目

我经常看到类似于下面的代码,尽管在切片容量可以预先得知的情况下依然生成一个容量为零的切片:var userIDs []string
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}这段代码中,切片的初始长度和容量都为零。在收到响应后,我们将用户添加到切片。这样做就会达到切片的容量上限,从而导致底层分配两倍容量的新数组,然后将旧切片中的数据拷贝过来。如果有8个用户,就会造成5次内存分配。

更有效的方式是这样:
userIDs := make([]string, 0, len(rsp.Users)
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}使用make显式声明切片的容量。接下来可以向切片添加元素,而不会触发内存重新分配和拷贝。
 
这条建议也适用于map:适用make(map[string]string, len(foo))可以分配足够的底层内存以避免内存重新分配。
 
使用可以接受字节切片的方法
 
在使用包时,寻找那些接受字节切片作为参数的方法,这些方法通常给你更多控制内存分配的自由。

一个很好的例子就是time.Format和time.AppendFormat。time.Format返回字符串。内部会分配一个新的字节切片,然后在其上调用time.AppendFormat。而time.AppendFormat接受一个字节缓冲区,将格式化后的时间写入缓冲区,然后返回扩展后的字节切片。标准库中这种做法非常常见,如strconv.AppendFloat或bytes.NewBuffer。

为什么这样能提高性能?因为你可以传递从sync.Poolh获得的字节切片,而不需要每次都分配新的缓冲区。或者可以初始化一个足够大的缓冲区,来减少切片拷贝。
 
这些建议只是某些具体情况下的建议,而不是真理。一定要自己测量性能。

要知道何时该停止优化。提高系统性能会让工程师感觉非常满足:问题本身很有趣,也有立竿见影的效果。但是,提高性能带来的效果非常依赖于具体情况。如果服务的响应时间只有10毫秒,而网络访问需要90毫秒,那么将10毫秒优化到5毫秒就完全不值得,因为你依然需要95毫秒。就算你将响应时间优化到1毫秒,最后结果还是91毫秒。你应该去做其他更有价值的事情。
 
原文:
https://stephen.sh/posts/quick-go-performance-improvements
译文:
https://blog.csdn.net/csdnnews/article/details/93987866
  查看全部
这篇文章中列出了一些不需要太多精力就能显著提高性能的技巧,并不包含那些需要太多精力或需要大幅度修改程序结构的技巧。
 
开始优化之前
 
开始优化之前,首先应该花些时间找出一个合适的基准线,以便稍后比较。如果没有基准,那就等于摸着石头过河,根本不知道自己的优化有没有效果。首先要编写性能测试程序,然后生成能用于pprof的profile文件。最好可以编写Go的性能测试脚本(https://dave.cheney.net/2013/0 ... in-go),这样可以很容易地使用pprof,还可以评测内存分配情况。还可以使用benchcmp,这个工具可以帮助比较两次性能测试之间的性能差异。

如果代码很难做性能测试,那就从你能测量时间的部分开始。可以利用runtime/pprof手工测量代码。

现在开始吧!
 
使用sync.Pool重用之前分配过的对象
 
sync.Pool实现了一个空闲列表(free-list)。这样可以重新使用之前分配过的对象。这样做可以将对象分配的代价平摊到多次使用上,减少垃圾回收器的工作。API非常简单:只需实现一个函数,用来分配新的对象即可。它会返回指针类型。
 
var bufpool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 512)
return &buf
}}
之后,可以用Get()从池中获取对象,用完之后用Put()将对象放回。

不过要注意一些陷阱。在Go 1.13之前,每次发生垃圾回收时该池都会被清空。对于需要分配大量对象的程序来说,这可能会造成性能的影响。在1.13版本中似乎GC后能保留更多对象了(https://go-review.googlesource.com/c/go/+/162919/)。

你可能需要在将对象放回池中之前将其结构的字段清空。

如果不这样做,就可能从池中获得一个“脏”的对象,它包含之前使用过的数据。这可能会造成严重的安全问题!
 
安全的做法就是明确清空内存:
// reset resets all fields of the AuthenticationResponse before pooling it.
func (a* AuthenticationResponse) reset() {
a.Token = ""
a.UserID = ""
}

rsp := authPool.Get().(*AuthenticationResponse)
defer func() {
rsp.reset()
authPool.Put(rsp)
}()

在大map中避免使用包含指针的结构作为map的键
 
关于Go中大型堆的性能问题已经有很多人讨论过了。在垃圾回收过程中,运行时会扫描包含指针的对象并遍历其指针。如果你有非常大的map[string]int,那么垃圾回收器就不得不在每次垃圾回收过程中检查map中的每个字符串,因为字符串包含指针。

这个例子中我们向一个map[string]int中写入了一千万个元素,然后测量垃圾回收的时间。map是在包的作用域中分配的,以保证它被分配到堆上。
import (
"fmt"
"runtime"
"strconv"
"time"
)

const (
numElements = 10000000
)

var foo = map[string]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[strconv.Itoa(i)] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}
运行后可以得到以下结果:
gc took: 98.726321ms
gc took: 105.524633ms
gc took: 102.829451ms
gc took: 102.71908ms
gc took: 103.084104ms
gc took: 104.821989ms
对于计算机来说这花得时间太多了!

怎样可以改进呢?最好是能尽量去掉指针,这样能减少垃圾回收器需要遍历的指针数量。由于字符串包含指针,因此我们可以用map[int]int来实现:
 
import (
"fmt"
"runtime"
"time"
)

const (
numElements = 10000000
)

var foo = map[int]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[i] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}
重新运行程序,结果如下:
 
gc took: 3.608993ms
gc took: 3.926913ms
gc took: 3.955706ms
gc took: 4.063795ms
gc took: 3.91519ms
gc took: 3.75226ms
好多了。垃圾回收的时间减少了97%。在生产环境下,字符串需要进行hash之后再插入到map中。
 
生成marshalling代码以避免运行时反射
 
将数据结构marshalh或unmarshal成JSON等各种序列化格式是个很常见的操作,特别是在构建微服务的时候。实际上,大部分微服务做的唯一工作就是序列化。像json.Marshal和json.Unmarshal需要依赖运行时反射才能将结构体的字段序列化成字节,反之亦然。这个操作很慢,反射的性能完全无法与显式的代码相比。

但我们不必这么做。marshalling JSON的原理大致如下:
package json

// Marshal take an object and returns its representation in JSON.
func Marshal(obj interface{}) ([]byte, error) {
// Check if this object knows how to marshal itself to JSON
// by satisfying the Marshaller interface.
if m, is := obj.(json.Marshaller); is {
return m.MarshalJSON()
}

// It doesn't know how to marshal itself. Do default reflection based marshallling.
return marshal(obj)
}
如果我们知道怎样将对象marshal成JSON,就应该避免运行时反射。但我们不想手工marshal所有代码,怎么办呢?可以让计算机替我们写程序!像easyjson等代码生成器会检查结构体,然后生成高度优化且与json.Marshaller等接口完全兼容的代码。

下载这个包,然后在包含结构体的$file.go上运行下面的命令:
easyjson -all $file.go$file.go

这个命令会生成$file_easyjson.go。由于easyjson为我们实现了json.Marshaller接口,因此序列化时不会调用默认的反射,而是会使用生成的函数。祝贺你!你已经将JSON marshalling的代码的速度提高了三倍。还有许多其他技巧可以进一步提升性能。
 
使用strings.Builder来构建字符串
 
Go语言的字符串是不可修改的,可以认为它们是只读的字节切片。这就是说,每次创建字符串都要分配新的内存,可能还会给垃圾回收器造成更多工作。

Go 1.10引入了strings.Builder作为高效率构建字符串的方式。它内部会将字符串写入到字节缓冲区。只有在builder上调用String()时才会真正生成字符串。它依赖一些unsafe的技巧将底层的字节作为字符串返回,而不实际进行内存非配。这篇文章(https://syslog.ravelin.com/byt ... ca7ff)介绍了更多其工作原理。
pkg: github.com/sjwhitworth/perfblog/strbuild
BenchmarkStringBuildNaive-8 5000000 255 ns/op 216 B/op 8 allocs/op
BenchmarkStringBuildBuilder-8 20000000 54.9 ns/op 64 B/op 1 allocs/op
可见,strings.Builder要快4.7倍,它的内存分配次只有前者的1/8,内存使用只有前者的1/4。

所以,在性能重要的时候应该使用strings.Builder。一般来说,除非是非常不重要的情况,否则我建议永远使用strings.Builder来构建字符串。
 
使用strconv代替fmt

fmt是Go中最著名的包之一。估计你从第一个Go程序——输出“hello, world”——的时候就开始用它了。但是,如果需要将整数和浮点数转换成字符串,它就不如更底层的strconv有效率了。strconv只需要在API中进行很小改动,就能带来不错的性能提升。

大多数情况下fmt会接受一个interface{}作为参数。这样做有两个弊端:

失去了类型安全。对于我来说这是个很大的问题。

会增加内存分配次数。将非指针类型作为interface{}传递通常会导致堆分配的问题。进一步的内容可以阅读这篇文章(https://www.darkcoding.net/sof ... face/)。
BenchmarkStrconv-8      30000000            39.5 ns/op        32 B/op          1 allocs/op
BenchmarkFmt-8 10000000 143 ns/op 72 B/op 3 allocs/op
可以看到,strconv版本要快3.5倍,内存分配次数是1/3,内存分配量是1/2。

在make中指定分配的容量来避免重新分配

在讨论性能改善之前,我们先来迅速看一下切片。切片是Go语言中一个非常有用的概念。它提供了可改变大小的数组,还可以用不同的方式表示同一片底层内存区域,而不需要重新进行内存分配。slice的内部结构由三个元素组成:


data:切片中指向底层数据的指针
len:切片中的当前元素数目
cap:在不重新分配内存的前提下,切片能够增长到的元素数目


我经常看到类似于下面的代码,尽管在切片容量可以预先得知的情况下依然生成一个容量为零的切片:
var userIDs []string
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}
这段代码中,切片的初始长度和容量都为零。在收到响应后,我们将用户添加到切片。这样做就会达到切片的容量上限,从而导致底层分配两倍容量的新数组,然后将旧切片中的数据拷贝过来。如果有8个用户,就会造成5次内存分配。

更有效的方式是这样:
userIDs := make([]string, 0, len(rsp.Users)
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}
使用make显式声明切片的容量。接下来可以向切片添加元素,而不会触发内存重新分配和拷贝。
 
这条建议也适用于map:适用make(map[string]string, len(foo))可以分配足够的底层内存以避免内存重新分配。
 
使用可以接受字节切片的方法
 
在使用包时,寻找那些接受字节切片作为参数的方法,这些方法通常给你更多控制内存分配的自由。

一个很好的例子就是time.Format和time.AppendFormat。time.Format返回字符串。内部会分配一个新的字节切片,然后在其上调用time.AppendFormat。而time.AppendFormat接受一个字节缓冲区,将格式化后的时间写入缓冲区,然后返回扩展后的字节切片。标准库中这种做法非常常见,如strconv.AppendFloat或bytes.NewBuffer。

为什么这样能提高性能?因为你可以传递从sync.Poolh获得的字节切片,而不需要每次都分配新的缓冲区。或者可以初始化一个足够大的缓冲区,来减少切片拷贝。
 
这些建议只是某些具体情况下的建议,而不是真理。一定要自己测量性能。

要知道何时该停止优化。提高系统性能会让工程师感觉非常满足:问题本身很有趣,也有立竿见影的效果。但是,提高性能带来的效果非常依赖于具体情况。如果服务的响应时间只有10毫秒,而网络访问需要90毫秒,那么将10毫秒优化到5毫秒就完全不值得,因为你依然需要95毫秒。就算你将响应时间优化到1毫秒,最后结果还是91毫秒。你应该去做其他更有价值的事情。
 
原文:
https://stephen.sh/posts/quick-go-performance-improvements
译文:
https://blog.csdn.net/csdnnews/article/details/93987866
 

理解Golang中的函数、方法、闭包的本质

zkbhj 发表了文章 • 0 个评论 • 106 次浏览 • 2020-07-24 10:27 • 来自相关话题

函数的本质

在go的世界中,函数是一等公民,可以给变量赋值,可以作为参数传递,也可以直接赋值。
在go语言中将这样的变量、参数、返回值,即在堆空间和栈空间中绑定函数的值,称为function value。
函数的指令在编译期间生成,使用go tool compile -S main.go可以获取汇编代码, 以OSX 10.15.6,go 1.14为例,将看到下述汇编代码(下面只引用部分)...
"".B STEXT nosplit size=1 args=0x8 locals=0x0
0x0000 00000 (main.go:9) TEXT "".B(SB), NOSPLIT|ABIInternal, $0-8
0x0000 00000 (main.go:9) PCDATA $0, $-2
0x0000 00000 (main.go:9) PCDATA $1, $-2
0x0000 00000 (main.go:9) FUNCDATA $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
0x0000 00000 (main.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:9) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:11) PCDATA $0, $-1
0x0000 00000 (main.go:11) PCDATA $1, $-1
0x0000 00000 (main.go:11) RET
0x0000 c3
...运行时将存放在__TEXT段中,也就是存放在代码段中,读写权限为rx/rwx, 通过vmmap [pid]可以获取运行时的内存分布

==== Non-writable regions for process 13443
REGION TYPE START - END [ VSIZE RSDNT DIRTY SWAP] PRT/MAX SHRMOD PURGE REGION DETAIL
__TEXT 0000000001000000-0000000001167000 [ 1436K 1436K 0K 0K] r-x/rwx SM=COW .../test

使用otool -v -l [file]可以看到下述内容(下面只引用了一部分)...
Load command 1
cmd LC_SEGMENT_64
cmdsize 632
segname __TEXT
vmaddr 0x0000000001000000
vmsize 0x0000000000167000
fileoff 0
filesize 1470464
maxprot rwx
initprot r-x
nsects 7
flags (none)
Section
sectname __text
segname __TEXT
addr 0x0000000001001000
size 0x000000000009c365
offset 4096
align 2^4 (16)
reloff 0
nreloc 0
type S_REGULAR
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
reserved1 0
reserved2 0
...所以如果要问函数在go语言里的本质是什么,那么其实就是指向__TEXT段内存地址的一个指针。
 
函数调用的过程

在go语言中,每一个goroutine持有一个连续栈,栈基础大小为2kb,当栈大小超过预分配大小后,会触发栈扩容,也就是分配一个大小为当前栈2倍的新栈,并且将原来的栈拷贝到新的栈上。使用连续栈而不是分段栈的目的是,利用局部性优势提升执行速度,原理是CPU读取地址时会将相邻的内存读取到访问速度比内存快的多级cache中,地址连续性越好,L1、L2、L3 cache命中率越高,速度也就越快。

在go中,和其他一些语言有所不同,函数的返回值、参数都是由被caller保存。每次函数调用时,会在caller的栈中压入函数返回值列表、参数列表、函数返回时的PC地址,然后更改bp和pc为新函数,执行新函数,执行完之后将变量存到caller的栈空间中,利用栈空间中保存的返回地址和caller的栈基地址,恢复pc和sp回到caller的执行过程。

对于栈变量的访问是通过bp+offset的方式来访问,而对于在堆上分配的变量来说,就是通过地址来访问。在go中,变量被分配到堆上还是被分配到栈上是由编译器在编译时根据逃逸分析决定的,不可以更改,只能利用规则尽量让变量被分配到栈上,因为局部性优势,栈空间的内存访问速度快于堆空间访问。
 方法的本质

go里面其实方法就是语法糖,请看下述代码,两个Println打印的结果是一样的,实际上Method就是将receiver作为函数的第一个参数输入的语法糖而已,本质上和函数没有区别。type T struct {
name string
}

func (t T) Name() string {
return "Hi! " + t.name
}

func main() {
t := T{name: "test"}
fmt.Println(t.Name()) // Hi! test
fmt.Println(T.Name(t)) // Hi! test
}
 闭包的本质

前面已经提到在go语言中将这在堆空间和栈空间中绑定函数的值,称为function value。这也就是闭包在go语言中的实体。一个最简单的funcval实际上是通过二级指针指向__TEXT代码段上函数的结构体。

那我们来看下面这个闭包,也就是main函数中的变量ffunc getFunc() func() int {
a := 0
return func() int {
a++
return a
}
}

func main() {
f := getFunc()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}上面这段代码执行完后会输出1~10,也就是说f在执行的时候所使用的a会累计,但是a并不是一个全局变量,为什么f就变成了一个有状态的函数呢?其实这也就是go里面的闭包了。那我们来看go是如何实现闭包的。

首先来解释一下闭包的含义,闭包在实现上是一个结构体,需要存储函数入口和关联环境,关联环境包含约束变量(函数内部变量)和自由变量(函数外部变量,在函数外被定义,但是在函数内被引用,如例子中的变量a),和函数不同的是,在捕获闭包时才能确定自由变量,当脱离了捕捉变量的上下文时,也能照常运行。基于闭包可以很容易的定义异步调用的回调函数。

在 go 语言中,闭包的状态是通过捕获列表实现的。具体来说,有自由变量的闭包funcval的分配都在堆上,(没有自由变量的funcval在__DATA数据段上,和常量一样),funcval中除了包含地址以外,还会包含所引用的自由变量,所有自由变量构成捕获列表。对于会被修改的值,捕获的是值的指针,对于不会被修改的值,捕获的是值拷贝。
 
https://mp.weixin.qq.com/s/96Df9YGykE6daDREli9W1A
  查看全部
函数的本质

在go的世界中,函数是一等公民,可以给变量赋值,可以作为参数传递,也可以直接赋值。
在go语言中将这样的变量、参数、返回值,即在堆空间和栈空间中绑定函数的值,称为function value。
函数的指令在编译期间生成,使用go tool compile -S main.go可以获取汇编代码, 以OSX 10.15.6,go 1.14为例,将看到下述汇编代码(下面只引用部分)
...
"".B STEXT nosplit size=1 args=0x8 locals=0x0
0x0000 00000 (main.go:9) TEXT "".B(SB), NOSPLIT|ABIInternal, $0-8
0x0000 00000 (main.go:9) PCDATA $0, $-2
0x0000 00000 (main.go:9) PCDATA $1, $-2
0x0000 00000 (main.go:9) FUNCDATA $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
0x0000 00000 (main.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:9) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:11) PCDATA $0, $-1
0x0000 00000 (main.go:11) PCDATA $1, $-1
0x0000 00000 (main.go:11) RET
0x0000 c3
...
运行时将存放在__TEXT段中,也就是存放在代码段中,读写权限为rx/rwx, 通过vmmap [pid]可以获取运行时的内存分布

==== Non-writable regions for process 13443
REGION TYPE START - END [ VSIZE RSDNT DIRTY SWAP] PRT/MAX SHRMOD PURGE REGION DETAIL
__TEXT 0000000001000000-0000000001167000 [ 1436K 1436K 0K 0K] r-x/rwx SM=COW .../test

使用otool -v -l [file]可以看到下述内容(下面只引用了一部分)
...
Load command 1
cmd LC_SEGMENT_64
cmdsize 632
segname __TEXT
vmaddr 0x0000000001000000
vmsize 0x0000000000167000
fileoff 0
filesize 1470464
maxprot rwx
initprot r-x
nsects 7
flags (none)
Section
sectname __text
segname __TEXT
addr 0x0000000001001000
size 0x000000000009c365
offset 4096
align 2^4 (16)
reloff 0
nreloc 0
type S_REGULAR
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
reserved1 0
reserved2 0
...
所以如果要问函数在go语言里的本质是什么,那么其实就是指向__TEXT段内存地址的一个指针。
 
函数调用的过程

在go语言中,每一个goroutine持有一个连续栈,栈基础大小为2kb,当栈大小超过预分配大小后,会触发栈扩容,也就是分配一个大小为当前栈2倍的新栈,并且将原来的栈拷贝到新的栈上。使用连续栈而不是分段栈的目的是,利用局部性优势提升执行速度,原理是CPU读取地址时会将相邻的内存读取到访问速度比内存快的多级cache中,地址连续性越好,L1、L2、L3 cache命中率越高,速度也就越快。

在go中,和其他一些语言有所不同,函数的返回值、参数都是由被caller保存。每次函数调用时,会在caller的栈中压入函数返回值列表、参数列表、函数返回时的PC地址,然后更改bp和pc为新函数,执行新函数,执行完之后将变量存到caller的栈空间中,利用栈空间中保存的返回地址和caller的栈基地址,恢复pc和sp回到caller的执行过程。

对于栈变量的访问是通过bp+offset的方式来访问,而对于在堆上分配的变量来说,就是通过地址来访问。在go中,变量被分配到堆上还是被分配到栈上是由编译器在编译时根据逃逸分析决定的,不可以更改,只能利用规则尽量让变量被分配到栈上,因为局部性优势,栈空间的内存访问速度快于堆空间访问
 方法的本质

go里面其实方法就是语法糖,请看下述代码,两个Println打印的结果是一样的,实际上Method就是将receiver作为函数的第一个参数输入的语法糖而已,本质上和函数没有区别。
type T struct {
name string
}

func (t T) Name() string {
return "Hi! " + t.name
}

func main() {
t := T{name: "test"}
fmt.Println(t.Name()) // Hi! test
fmt.Println(T.Name(t)) // Hi! test
}

 闭包的本质

前面已经提到在go语言中将这在堆空间和栈空间中绑定函数的值,称为function value。这也就是闭包在go语言中的实体。一个最简单的funcval实际上是通过二级指针指向__TEXT代码段上函数的结构体。

那我们来看下面这个闭包,也就是main函数中的变量f
func getFunc() func() int {
a := 0
return func() int {
a++
return a
}
}

func main() {
f := getFunc()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
上面这段代码执行完后会输出1~10,也就是说f在执行的时候所使用的a会累计,但是a并不是一个全局变量,为什么f就变成了一个有状态的函数呢?其实这也就是go里面的闭包了。那我们来看go是如何实现闭包的。

首先来解释一下闭包的含义,闭包在实现上是一个结构体,需要存储函数入口和关联环境,关联环境包含约束变量(函数内部变量)和自由变量(函数外部变量,在函数外被定义,但是在函数内被引用,如例子中的变量a),和函数不同的是,在捕获闭包时才能确定自由变量,当脱离了捕捉变量的上下文时,也能照常运行。基于闭包可以很容易的定义异步调用的回调函数。

在 go 语言中,闭包的状态是通过捕获列表实现的。具体来说,有自由变量的闭包funcval的分配都在堆上,(没有自由变量的funcval在__DATA数据段上,和常量一样),funcval中除了包含地址以外,还会包含所引用的自由变量,所有自由变量构成捕获列表。对于会被修改的值,捕获的是值的指针,对于不会被修改的值,捕获的是值拷贝
 
https://mp.weixin.qq.com/s/96Df9YGykE6daDREli9W1A
 

Golang中的并发安全

zkbhj 发表了文章 • 0 个评论 • 121 次浏览 • 2020-07-23 17:05 • 来自相关话题

并发安全

并发安全也叫线程安全,在并发中出现了数据的丢失,称为并发不安全。

map和slice都是并发不安全的。
 

slice在并发执行中不会报错,但是数据会丢失;
map在并发执行中会直接报错(fatal error: concurrent map writes)。

 
切片并发不安全

场景: 10000个协程同时添加切片
var s []int

func appendValue(i int) {
s = append(s, i)
}

func main() {
for i := 0; i < 10000; i++ { //10000个协程同时添加切片
go appendValue(i)
}

for i, v := range s { //同时打印索引和值
fmt.Println(i, ":", v)
}
}输出:

 
没有到9999,说明有数据丢失

解决方法: 加锁
var s []int
var lock sync.Mutex //互斥锁
func appendValue(i int) {
lock.Lock() //加锁
s = append(s, i)
lock.Unlock() //解锁
}

func main() {
for i := 0; i < 10000; i++ {
go appendValue(i)
}
//sort.Ints(s) //给切片排序,先排完序再打印,和下面一句效果相同
time.Sleep(time.Second) //间隔1s再打印,防止一边插入数据一边打印时数据乱序
for i, v := range s {
fmt.Println(i, ":", v)
}
}
总结: slice在并发执行中不会报错,但是数据会丢失

map并发不安全

场景: 2个协程同时读和写
func main() {
m := make(map[int]int)
go func() { //开一个协程写map
for i := 0; i < 10000; i++ {
m[i] = i
}
}()

go func() { //开一个协程读map
for i := 0; i < 10000; i++ {
fmt.Println(m[i])
}
}()

//time.Sleep(time.Second * 20)
for {
;
}
}输出:

 
解决方法:尽量不要做map的并发,如果用并发要加锁,保证map的操作要么读,要么写。
var lock sync.Mutex
func main() {
m:=make(map[int]int)
go func() { //开一个协程写map
for i:=0;i<10000 ;i++ {
lock.Lock() //加锁
m[i]=i
lock.Unlock() //解锁
}
}()
go func() { //开一个协程读map
for i:=0;i<10000 ;i++ {
lock.Lock() //加锁
fmt.Println(m[i])
lock.Unlock() //解锁
}
}()
time.Sleep(time.Second*20)
}

总结: map在并发执行中会直接报错

原文链接:https://blog.csdn.net/weixin_4 ... 97247 查看全部
并发安全

并发安全也叫线程安全,在并发中出现了数据的丢失,称为并发不安全。

map和slice都是并发不安全的。
 


slice在并发执行中不会报错,但是数据会丢失;
map在并发执行中会直接报错(fatal error: concurrent map writes)。


 
切片并发不安全

场景: 10000个协程同时添加切片
var s []int

func appendValue(i int) {
s = append(s, i)
}

func main() {
for i := 0; i < 10000; i++ { //10000个协程同时添加切片
go appendValue(i)
}

for i, v := range s { //同时打印索引和值
fmt.Println(i, ":", v)
}
}
输出:

 
没有到9999,说明有数据丢失

解决方法: 加锁
var s []int
var lock sync.Mutex //互斥锁
func appendValue(i int) {
lock.Lock() //加锁
s = append(s, i)
lock.Unlock() //解锁
}

func main() {
for i := 0; i < 10000; i++ {
go appendValue(i)
}
//sort.Ints(s) //给切片排序,先排完序再打印,和下面一句效果相同
time.Sleep(time.Second) //间隔1s再打印,防止一边插入数据一边打印时数据乱序
for i, v := range s {
fmt.Println(i, ":", v)
}
}
总结: slice在并发执行中不会报错,但是数据会丢失

map并发不安全

场景: 2个协程同时读和写
func main() {
m := make(map[int]int)
go func() { //开一个协程写map
for i := 0; i < 10000; i++ {
m[i] = i
}
}()

go func() { //开一个协程读map
for i := 0; i < 10000; i++ {
fmt.Println(m[i])
}
}()

//time.Sleep(time.Second * 20)
for {
;
}
}
输出:

 
解决方法:尽量不要做map的并发,如果用并发要加锁,保证map的操作要么读,要么写。
var lock sync.Mutex
func main() {
m:=make(map[int]int)
go func() { //开一个协程写map
for i:=0;i<10000 ;i++ {
lock.Lock() //加锁
m[i]=i
lock.Unlock() //解锁
}
}()
go func() { //开一个协程读map
for i:=0;i<10000 ;i++ {
lock.Lock() //加锁
fmt.Println(m[i])
lock.Unlock() //解锁
}
}()
time.Sleep(time.Second*20)
}

总结: map在并发执行中会直接报错

原文链接:https://blog.csdn.net/weixin_4 ... 97247

如何用守护进程启动Beego应用?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 406 次浏览 • 2020-07-09 20:04 • 来自相关话题

生产者消费者模型是什么?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 454 次浏览 • 2020-06-19 17:26 • 来自相关话题

#2020学习打卡##Go语言高级编程# Golang的内存模型

zkbhj 发表了文章 • 0 个评论 • 213 次浏览 • 2020-06-17 12:14 • 来自相关话题

什么是内存模型

首先内存模型并不是指arena/spans/bitmap(如下图)。这些是内存划分。





 
为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。

通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。

它解决了 CPU 多级缓存、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

上面提到,内存模型与处理器有关、与缓存有关、与并发有关、与编译器也有关,那么我们在编写Go程序的时候,需要去了解CPU等底层特性吗?其实是不需要的!因为内存模型是抽象的,在不同的平台下,编译器会生成合适的内存屏障,帮我们屏蔽了底层的差异。这里将面向抽象编程的思想体现的淋漓尽致! 
Golang的内存模型
 
Happens Before 是内存模型中一个通用的概念,Go 中也定义了Happens Before以及各种发生Happens Before关系的操作,因为有了这些Happens Before操作的保证,我们写的多goroutine的程序才会按照我们期望的方式来工作。

什么是Happens Before关系

Happens Before定义了两个操作间的偏序关系,具有传递性。对于两个操作E1和E2: 
如果E1 Happens Before E2, 则E2 Happens After E1;如果E1 Happens E2, E2 Happens Before E3,则E1 Happens E3;如果 E1 和 E2没有任何Happens Before关系,则说E1和E2 Happen Concurrently。

Happens Before的作用

Happens Before主要是用来保证内存操作的可见性。如果要保证E1的内存写操作能够被E2读到,那么需要满足:
E1 Happens Before E2;其他所有针对此内存的写操作,要么Happens Before E1,要么Happens After E2。也就是说不能存在其他的一个写操作E3,这个E3 Happens Concurrently E1/E2。

为什么需要定义Happens Before关系来保证内存操作的可见性呢?原因是没有限制的情况下,编译器和CPU使用的各种优化,会对此造成影响,具体的来说就是操作重排序和CPU CacheLine缓存同步:
操作重排序。现代CPU通常是流水线架构,且具有多个核心,这样多条指令就可以同时执行。然而有时候出现一条指令需要等待之前指令的结果,或是其他造成指令执行需要延迟的情况。这个时候可以先执行下一条已经准备好的指令,以尽可能高效的利用CPU。操作重排序可以在两个阶段出现:
  
编译器指令重排序CPU乱序执行
 
CPU 多核心间独立Cache Line的同步问题。多核CPU通常有自己的一级缓存和二级缓存,访问缓存的数据很快。但是如果缓存没有同步到主存和其他核心的缓存,其他核心读取缓存就会读到过期的数据。

举例来说,看一个多Goroutine的程序:// Sample Routine 1
func happensBeforeMulti(i int) {
i += 2 // E1
go func() { // G1 goroutine create
fmt.Println(i) // E2
}() // G2 goroutine destryo
}对此来讲解:
如果编译器或者CPU进行了重排序,那么E1的指令可能在E2之后执行,从而输出错误的值;变量i被CPU缓存到Cache Line中,E1对i的修改只改写了Cache Line,没有写回主存;而E2在另外的goroutine执行,如果和E1不是在同一个核上,那么E2输出的就是错误的值。

而Happens Before关系,就是对编译器和CPU的限制,禁止违反Happens Before关系的指令重排序及乱序执行行为,以及必要的情况下保证CacheLine的数据更新等。
 
Go 中定义的Happens Before保证

1) 单线程

在单线程环境下,所有的表达式,按照代码中的先后顺序,具有Happens Before关系。

CPU和正确实现的编译器,对单线程情况下的Happens Before关系,都是有保障的。这并不是说编译器或者CPU不能做重排序,只要优化没有影响到Happens Before关系就是可以的。这个依据在于分析数据的依赖性,数据没有依赖的操作可以重排序。

比如以下程序:// Sample Routine 2
func happsBefore(i int, j int) {
i += 2 // E1
j += 10 // E2
fmt.Println(i + j) //E3
}E1和E2之间,执行顺序是没有关系的,只要保证E3没有被乱序到E1和E2之前执行就可以。

2) Init 函数

如果包P1中导入了包P2,则P2中的init函数Happens Before 所有P1中的操作
main函数Happens After 所有的init函数

3) Goroutine

Goroutine的创建Happens Before所有此Goroutine中的操作
Goroutine的销毁Happens After所有此Goroutine中的操作

我们上面提到的Sample Routine 1,按照规则1, E1 Happens before G1,按照本规则,G1 Happens Before E2,从而E1 Happens Before E2。

4) Channel
对一个元素的send操作Happens Before对应的receive 完成操作对channel的close操作Happens Before receive 端的收到关闭通知操作对于Unbuffered Channel,对一个元素的receive 操作Happens Before对应的send完成操作对于Buffered Channel,假设Channel 的buffer 大小为C,那么对第k个元素的receive操作,Happens Before第k+C个send完成操作。可以看出上一条Unbuffered Channel规则就是这条规则C=0时的特例

首先注意这里面,send和send完成,这是两个事件,receive和receive完成也是两个事件。

然后,Buffered Channel这里有个坑,它的Happens Before保证比UnBuffered 弱,这个弱只在【在receive之前写,在send之后读】这种情况下有问题。而【在send之前写,在receive之后读】,这样用是没问题的,这也是我们通常写程序常用的模式,千万注意这里不要弄错!// Channel routine 1
var c = make(chan int)
var a string

func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}// Channel routine 2
var c = make(chan int, 10)
var a string

func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}// Channel routine 3
var c = make(chan int, 10)
var a string

func f() {
a = "hello, world"
c <- 0
}

func main() {
go f()
<-c
print(a)
}比如上面这三个程序,使用channel来做同步,程序1和程序3是能够保证Happens Before关系的,程序2则不能够,也就是程序可能不会按照期望输出"hello, world"。

5) Lock

Go里面有Mutex和RWMutex两种锁,RWMutex除了支持互斥的Lock/Unlock,还支持共享的RLock/RUnlock。
对于一个Mutex/RWMutex,设n < m,则第n个Unlock操作Happens Before第m个Lock操作。对于一个RWMutex,存在数值n,RLock操作Happens After 第n个UnLock,其对应的RUnLockHappens Before 第n+1个Lock操作。

简单理解就是这一次的Lock总是Happens After上一次的Unlock,读写锁的RLock HappensAfter上一次的UnLock,其对应的RUnlock Happens Before 下一次的Lock。

6) Once

once.Do中执行的操作,Happens Before 任何一个once.Do调用的返回。

如果你对JVM的内存模型及定义的Happens Before关系都有所了解,那么这里对Go的内存模型的讲解与之非常类似,理解起来会非常容易。太阳底下无新鲜事,了解了一种语言的内存模型设计,其他类似的语言也就都可以很容易的理解了。如果是前端或者使用node的程序员,那么你压根就不需要清楚这些,毕竟始终只有一个线程在跑是吧。
 
扩展阅读:
https://zhuanlan.zhihu.com/p/29108170
https://studygolang.com/articles/22523
https://www.jianshu.com/p/2a43997c5d2e
https://www.jianshu.com/p/b9186dbebe8e
  查看全部
什么是内存模型

首先内存模型并不是指arena/spans/bitmap(如下图)。这些是内存划分。

QQ截图20200617120119.jpg

 
为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范

通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。

它解决了 CPU 多级缓存、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

上面提到,内存模型与处理器有关、与缓存有关、与并发有关、与编译器也有关,那么我们在编写Go程序的时候,需要去了解CPU等底层特性吗?其实是不需要的!因为内存模型是抽象的,在不同的平台下,编译器会生成合适的内存屏障,帮我们屏蔽了底层的差异。这里将面向抽象编程的思想体现的淋漓尽致! 
Golang的内存模型
 
Happens Before 是内存模型中一个通用的概念,Go 中也定义了Happens Before以及各种发生Happens Before关系的操作,因为有了这些Happens Before操作的保证,我们写的多goroutine的程序才会按照我们期望的方式来工作。

什么是Happens Before关系

Happens Before定义了两个操作间的偏序关系,具有传递性。对于两个操作E1和E2: 
  1. 如果E1 Happens Before E2, 则E2 Happens After E1;
  2. 如果E1 Happens E2, E2 Happens Before E3,则E1 Happens E3;
  3. 如果 E1 和 E2没有任何Happens Before关系,则说E1和E2 Happen Concurrently。


Happens Before的作用

Happens Before主要是用来保证内存操作的可见性。如果要保证E1的内存写操作能够被E2读到,那么需要满足:
  1. E1 Happens Before E2;
  2. 其他所有针对此内存的写操作,要么Happens Before E1,要么Happens After E2。也就是说不能存在其他的一个写操作E3,这个E3 Happens Concurrently E1/E2。


为什么需要定义Happens Before关系来保证内存操作的可见性呢?原因是没有限制的情况下,编译器和CPU使用的各种优化,会对此造成影响,具体的来说就是操作重排序和CPU CacheLine缓存同步:
  • 操作重排序。现代CPU通常是流水线架构,且具有多个核心,这样多条指令就可以同时执行。然而有时候出现一条指令需要等待之前指令的结果,或是其他造成指令执行需要延迟的情况。这个时候可以先执行下一条已经准备好的指令,以尽可能高效的利用CPU。操作重排序可以在两个阶段出现:

  
  1. 编译器指令重排序
  2. CPU乱序执行

 
  • CPU 多核心间独立Cache Line的同步问题。多核CPU通常有自己的一级缓存和二级缓存,访问缓存的数据很快。但是如果缓存没有同步到主存和其他核心的缓存,其他核心读取缓存就会读到过期的数据。


举例来说,看一个多Goroutine的程序:
// Sample Routine 1
func happensBeforeMulti(i int) {
i += 2 // E1
go func() { // G1 goroutine create
fmt.Println(i) // E2
}() // G2 goroutine destryo
}
对此来讲解:
  1. 如果编译器或者CPU进行了重排序,那么E1的指令可能在E2之后执行,从而输出错误的值;
  2. 变量i被CPU缓存到Cache Line中,E1对i的修改只改写了Cache Line,没有写回主存;而E2在另外的goroutine执行,如果和E1不是在同一个核上,那么E2输出的就是错误的值。


而Happens Before关系,就是对编译器和CPU的限制,禁止违反Happens Before关系的指令重排序及乱序执行行为,以及必要的情况下保证CacheLine的数据更新等。
 
Go 中定义的Happens Before保证

1) 单线程

在单线程环境下,所有的表达式,按照代码中的先后顺序,具有Happens Before关系。

CPU和正确实现的编译器,对单线程情况下的Happens Before关系,都是有保障的。这并不是说编译器或者CPU不能做重排序,只要优化没有影响到Happens Before关系就是可以的。这个依据在于分析数据的依赖性,数据没有依赖的操作可以重排序。

比如以下程序:
// Sample Routine 2
func happsBefore(i int, j int) {
i += 2 // E1
j += 10 // E2
fmt.Println(i + j) //E3
}
E1和E2之间,执行顺序是没有关系的,只要保证E3没有被乱序到E1和E2之前执行就可以。

2) Init 函数

如果包P1中导入了包P2,则P2中的init函数Happens Before 所有P1中的操作
main函数Happens After 所有的init函数

3) Goroutine

Goroutine的创建Happens Before所有此Goroutine中的操作
Goroutine的销毁Happens After所有此Goroutine中的操作

我们上面提到的Sample Routine 1,按照规则1, E1 Happens before G1,按照本规则,G1 Happens Before E2,从而E1 Happens Before E2。

4) Channel
  • 对一个元素的send操作Happens Before对应的receive 完成操作
  • 对channel的close操作Happens Before receive 端的收到关闭通知操作
  • 对于Unbuffered Channel,对一个元素的receive 操作Happens Before对应的send完成操作
  • 对于Buffered Channel,假设Channel 的buffer 大小为C,那么对第k个元素的receive操作,Happens Before第k+C个send完成操作。可以看出上一条Unbuffered Channel规则就是这条规则C=0时的特例


首先注意这里面,send和send完成,这是两个事件,receive和receive完成也是两个事件。

然后,Buffered Channel这里有个坑,它的Happens Before保证比UnBuffered 弱,这个弱只在【在receive之前写,在send之后读】这种情况下有问题。而【在send之前写,在receive之后读】,这样用是没问题的,这也是我们通常写程序常用的模式,千万注意这里不要弄错!
// Channel routine 1
var c = make(chan int)
var a string

func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
// Channel routine 2
var c = make(chan int, 10)
var a string

func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
// Channel routine 3
var c = make(chan int, 10)
var a string

func f() {
a = "hello, world"
c <- 0
}

func main() {
go f()
<-c
print(a)
}
比如上面这三个程序,使用channel来做同步,程序1和程序3是能够保证Happens Before关系的,程序2则不能够,也就是程序可能不会按照期望输出"hello, world"。

5) Lock

Go里面有Mutex和RWMutex两种锁,RWMutex除了支持互斥的Lock/Unlock,还支持共享的RLock/RUnlock。
  • 对于一个Mutex/RWMutex,设n < m,则第n个Unlock操作Happens Before第m个Lock操作。
  • 对于一个RWMutex,存在数值n,RLock操作Happens After 第n个UnLock,其对应的RUnLockHappens Before 第n+1个Lock操作。


简单理解就是这一次的Lock总是Happens After上一次的Unlock,读写锁的RLock HappensAfter上一次的UnLock,其对应的RUnlock Happens Before 下一次的Lock。

6) Once

once.Do中执行的操作,Happens Before 任何一个once.Do调用的返回。

如果你对JVM的内存模型及定义的Happens Before关系都有所了解,那么这里对Go的内存模型的讲解与之非常类似,理解起来会非常容易。太阳底下无新鲜事,了解了一种语言的内存模型设计,其他类似的语言也就都可以很容易的理解了。如果是前端或者使用node的程序员,那么你压根就不需要清楚这些,毕竟始终只有一个线程在跑是吧。
 
扩展阅读:
https://zhuanlan.zhihu.com/p/29108170
https://studygolang.com/articles/22523
https://www.jianshu.com/p/2a43997c5d2e
https://www.jianshu.com/p/b9186dbebe8e
 

#Go即将支持泛型#关于泛型,你应该了解的一些细节

zkbhj 发表了文章 • 0 个评论 • 246 次浏览 • 2020-06-17 10:48 • 来自相关话题

什么是泛型?

“泛型是程序设计语言的一种特性。允许程序员在强类型程序设计语言中编写代码时定义一些可变部分,那些部分在使用前必须作出指明。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。将类型参数化以达到代码复用提高软件开发工作效率的一种数据类型。泛型类是引用类型,是堆对象,主要是引入了类型参数这个概念。”

 什么是自然语言?

“自然语言通常是指一种自然地随文化演化的语言。英语、汉语、日语为自然语言的例子,而世界语则为人造语言,即是一种由人蓄意为某些特定目的而创造的语言。 不过,有时所有人类使用的语言(包括上述自然地随文化演化的语言,以及人造语言) 都会被视为“自然”语言,以相对于如编程语言等为计算机而设的“人造”语言。这一种用法可见于自然语言处理一词中。自然语言是人类交流和思维的主要工具。 自然语言是人类智慧的结晶,自然语言处理是人工智能中最为困难的问题之一,而对自然语言处理的研究也是充满魅力和挑战的。 ”


这是百度百科的解释,通俗地说我们日常交流使用的语言都是自然语言,比如汉语、英语、法语、藏语等等。
 
什么是程序设计语言?

“程序设计语言,programming language。用于书写计算机程序的语言。语言的基础是一组记号和一组规则。根据规则由记号构成的记号串的总体就是语言。在程序设计语言中,这些记号 串就是程序。程序设计语言有3个方面的因素,即语法、语义和语用。语法表示程序的结构或形式,亦即表示构成语言的各个记号之间的组合规律,但不涉及这些记 号的特定含义,也不涉及使用者。语义表示程序的含义,亦即表示按照各种方法所表示的各个记号的特定含义,但不涉及使用者。语用表示程序与使用者的关系。 ”

 泛型不是自然语言里的概念,那么它们之间有关系吗?泛型在自然语言里找到的出处吗?
 
有关系。因为泛型是面向对象里的概念,而面向对象是一种对现实世界理解和抽象的方法,自然语言也是对现实世界的一种理解,所以它们之间是有关系的。
 
比如这么一段程序,就实现了泛型。class Test<T>
{
public T obj;
public Set(T t)
{
this.obj = t;
}
}
比如说C#里的List,它是一个泛型类,把它翻译成中文就是列表。

List<T> ;

T是占位类型。List就像是一个容器,可以向里面放任何类型。

创建一个List是这样List<string> list = new List<string>();

如果有一个学生类型,那么可以这样List<学生> list ;

如果用中文表示,可以这样声明 列表<学生>list,

去掉符号就是 学生列表list

“学生列表”这是符合自然语言的偏正短语。

这样就证明了自然语言是支持泛型。最大的不同是类型名称和占位类型的前后位置不同,在程序设计语言是列表<学生>,在自然语言中是学生列表。

自然语言也支持两个泛型参数的泛型类。

比如Dictionary,根据它的功能用准确点的称呼“键值表”。比如声明一个Dictionary<姓名,学生>,就是声明一个姓名学生键值表。“姓名学生键值表”这也是符合汉语语法的短语。

所以自然语言是支持程序设计语言中的泛型的。
 
重要说明~
 
泛型,一般指编译期间,由编译器去认知的类型识别。注意两个词:时间点--“编译期”,服务目标--“编译器”。编译器干完活后,出来的就是精准而无冗余的代码

另一种叫“运行时”识别,消耗一定的内存空间,来存放和记录类型属性,这种一般不叫泛型。比如 Go 的 interface 可认为就是这种。 查看全部
什么是泛型?


“泛型是程序设计语言的一种特性。允许程序员在强类型程序设计语言中编写代码时定义一些可变部分,那些部分在使用前必须作出指明。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。将类型参数化以达到代码复用提高软件开发工作效率的一种数据类型。泛型类是引用类型,是堆对象,主要是引入了类型参数这个概念。”


 什么是自然语言?


“自然语言通常是指一种自然地随文化演化的语言。英语、汉语、日语为自然语言的例子,而世界语则为人造语言,即是一种由人蓄意为某些特定目的而创造的语言。 不过,有时所有人类使用的语言(包括上述自然地随文化演化的语言,以及人造语言) 都会被视为“自然”语言,以相对于如编程语言等为计算机而设的“人造”语言。这一种用法可见于自然语言处理一词中。自然语言是人类交流和思维的主要工具。 自然语言是人类智慧的结晶,自然语言处理是人工智能中最为困难的问题之一,而对自然语言处理的研究也是充满魅力和挑战的。 ”



这是百度百科的解释,通俗地说我们日常交流使用的语言都是自然语言,比如汉语、英语、法语、藏语等等。
 
什么是程序设计语言?


“程序设计语言,programming language。用于书写计算机程序的语言。语言的基础是一组记号和一组规则。根据规则由记号构成的记号串的总体就是语言。在程序设计语言中,这些记号 串就是程序。程序设计语言有3个方面的因素,即语法、语义和语用。语法表示程序的结构或形式,亦即表示构成语言的各个记号之间的组合规律,但不涉及这些记 号的特定含义,也不涉及使用者。语义表示程序的含义,亦即表示按照各种方法所表示的各个记号的特定含义,但不涉及使用者。语用表示程序与使用者的关系。 ”


 泛型不是自然语言里的概念,那么它们之间有关系吗?泛型在自然语言里找到的出处吗?
 
有关系。因为泛型是面向对象里的概念,而面向对象是一种对现实世界理解和抽象的方法,自然语言也是对现实世界的一种理解,所以它们之间是有关系的。
 
比如这么一段程序,就实现了泛型。
class Test<T>
{
public T obj;
public Set(T t)
{
this.obj = t;
}
}

比如说C#里的List,它是一个泛型类,把它翻译成中文就是列表。

List<T> ;

T是占位类型。List就像是一个容器,可以向里面放任何类型。

创建一个List是这样List<string> list = new List<string>();

如果有一个学生类型,那么可以这样List<学生> list ;

如果用中文表示,可以这样声明 列表<学生>list,

去掉符号就是 学生列表list

“学生列表”这是符合自然语言的偏正短语。

这样就证明了自然语言是支持泛型。最大的不同是类型名称和占位类型的前后位置不同,在程序设计语言是列表<学生>,在自然语言中是学生列表。

自然语言也支持两个泛型参数的泛型类。

比如Dictionary,根据它的功能用准确点的称呼“键值表”。比如声明一个Dictionary<姓名,学生>,就是声明一个姓名学生键值表。“姓名学生键值表”这也是符合汉语语法的短语。

所以自然语言是支持程序设计语言中的泛型的。
 
重要说明~
 
泛型,一般指编译期间,由编译器去认知的类型识别。注意两个词:时间点--“编译期”,服务目标--“编译器”。编译器干完活后,出来的就是精准而无冗余的代码

另一种叫“运行时”识别,消耗一定的内存空间,来存放和记录类型属性,这种一般不叫泛型。比如 Go 的 interface 可认为就是这种。

顺序一致性内存模型是个啥

zkbhj 发表了文章 • 0 个评论 • 237 次浏览 • 2020-06-16 11:10 • 来自相关话题

顺序一致性是多线程环境下的理论参考模型,为程序提供了极强的内存可见性保证,在顺序一致性执行过程中,所有动作之间的先后关系与程序代码的顺序一致。
 
特性
 
一个线程中的所有操作必定按照程序的顺序来执行。所有的线程都只能看到一个单一的执行顺序,不管是否同步。每个操作都必须原子执行且立即对所有程序可见。
 
举例
 
假设有两个线程A和B并发执行。其中A线程有3个操作,他们在程序中的顺序是:A1→A2→A3。B线程也有3个操作,他们在程序中的顺序是:B1→B2→B3。

假设这两个线程使用监视器锁来正确同步:A线程的3个操作执行后释放监视器锁,随后B线程获取同一个监视器锁。那么程序在顺序一致性模型中的执行效果将如下图所示。





加锁
 
现在我们再假设这两个线程没有做同步,下面是这个未同步程序在顺序一致性模型中的执行示意图,如下图所示。





未加锁
 
未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程A和B看到的执行顺序都是:B1→A1→A2→B2→A3→B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。
 
参考文档
https://blog.csdn.net/en_joker ... 24331
  查看全部
顺序一致性是多线程环境下的理论参考模型,为程序提供了极强的内存可见性保证,在顺序一致性执行过程中,所有动作之间的先后关系与程序代码的顺序一致。
 
特性
 
  1. 一个线程中的所有操作必定按照程序的顺序来执行。
  2. 所有的线程都只能看到一个单一的执行顺序,不管是否同步。
  3. 每个操作都必须原子执行且立即对所有程序可见。

 
举例
 
假设有两个线程A和B并发执行。其中A线程有3个操作,他们在程序中的顺序是:A1→A2→A3。B线程也有3个操作,他们在程序中的顺序是:B1→B2→B3。

假设这两个线程使用监视器锁来正确同步:A线程的3个操作执行后释放监视器锁,随后B线程获取同一个监视器锁。那么程序在顺序一致性模型中的执行效果将如下图所示。

201910291005_1.png

加锁
 
现在我们再假设这两个线程没有做同步,下面是这个未同步程序在顺序一致性模型中的执行示意图,如下图所示。

201910291005_2.png

未加锁
 
未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程A和B看到的执行顺序都是:B1→A1→A2→B2→A3→B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。
 
参考文档
https://blog.csdn.net/en_joker ... 24331
 

#2020学习打卡##Go语言高级编程# 什么是第一类对象?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 332 次浏览 • 2020-06-09 17:23 • 来自相关话题