Jquery中文网 www.jquerycn.cn
Jquery中文网 >  后端编程  >  Go语言  >  正文 golang int64 排序_Golang的内存模型

golang int64 排序_Golang的内存模型

发布时间:2021-05-21   编辑:www.jquerycn.cn
jquery中文网为您提供golang int64 排序,Golang的内存模型等资源,欢迎您收藏本站,我们将为您提供最新的golang int64 排序,Golang的内存模型资源

本文来自小天同学的投稿,233作为一个不会Golang的Java渣渣都看懂一些了。看完只能感慨是时候开始学Go了!强烈推荐给正在学习Golang或者对Golang感兴趣的小伙伴,建议收藏~

前言

本文主要是对The Go Memory Model一文的翻译,也想借此机会加深对golang内存模型的理解。

介绍

go内存模型规定了某个goroutine的读操作保证能观测到来自其他goroutine对这个变量的写操作的一组条件。

建议

那些数据在修改的同时如果被其他goroutine访问到,必须串行化(serialize)这种访问才能保证数据的安全性。

为了串行化访问,通过channel或者其他同步原语比如syncsync/atomic包来保护数据。

如果你必须阅读这篇文档剩下的内容以理解你的程序的行为,你将会变得太聪明。

永远不要聪明。

Happens Before

在单个goroutine里,读和写必须表现得好像它们在按照程序指定的顺序执行。

也就是,编译器和处理器可能重排序(reorder)单个goroutine内执行的读和写,当然重排序不会改变语言规范所定义的在这个goroutine内的行为。因为重排序的存在,一个goroutine观测到的执行顺序可能和其他goroutine观测到的不同。

举个栗子,如果一个goroutine执行a = 1; b = 2;另外一个goroutine可能观测到对变量b的更新先于a

为了规定读和写的依赖,我们定义了happens before,一个部分有序的执行

如果事件e1 happens before 事件e2,那我们说e2 happens after e1。另外,如果e1没有happens before e2同时e2没有happens before e1,那我们说e1和e2并发地发生。

在单goroutine环境,happens-before顺序就是程序所表达的顺序。

在下面两个条件满足时,对变量v的读r允许观测到对v的写w:

1. r没有happen before w

2. 没有其他对v的写w' happens after w并happen before r

保证对变量v的读观测到对v的特定写w,w是唯一允许被r观测到的写。即是,r被保证观测到w需要同时满足下列两个条件:

1. w happens before  r

2. 任何其他对变量v的写要么happens before w要么happens after r

这对条件比第一对更强,它需要没有其他写和w或者r并发地发生。

笔记:第一对条件是允许观测,第二对条件是保证观测,约束强度不一样。

在单goroutine环境,没有并发,所以这两个定义是等价的:一个读r观测到最近的对这个变量的写w。

当多个goroutine同时访问一个共享变量v,它们必须使用同步事件来建立happens-before条件以保证读观测到想要的写。

对变量v的零值初始化的行为像内存模型的写。

对大于一个机器字word的变量的读和写的行为像多机器字尺寸(multiple machine-word-sized)操作一样未定义顺序。

笔记:比如32位系统的一个字word是4byte,对int64类型的变量v的写和读,可能发生goroutine1写了4个byte,goroutine2读了v的值,goroutine1继续写v剩下的4个byte这种顺序

同步

初始化

程序开始时运行在单个goroutine,但是这个goroutine可能创建其他goroutine并发运行。

  • 如果一个包p导入包q,q的init函数的完成happens before任何p的init开始前。

  • main.main函数的开始happens after所有init函数已经结束。

goroutine创建

  • 开始一个新goroutine的go语句happens before这个goroutine执行开始*

比如下面这个栗子:


var a string

func f() {

 print(a)

}

func hello() {

Step1 :  a = "hello, world"

Step2 :  go f()

}

调用hello一定会打印"hello, world",后者发生在a赋值的未来的某个时间点(可能在hello函数返回后)。

goroutine销毁

goroutine的退出不保证happens before程序中的任何事件。比如下面这个程序:


var a string

func hello() {

 go func() { a = "hello" }()

 print(a)

}

这个赋值没有跟随任何同步事件,所以不保证被任何其他goroutine观测到。实际上,激进的编译器可能删除这整个go语句(go statemennt,即go func() { a = "hello" }()这行)。

如果一个goroutine的作用必须被其他goroutine观测到,使用一个同步机制比如一个锁或者channel通信来建立相对关系。

channel通信

channel通信是goroutine间主要的同步方法。任何在一个特定channel的send匹配一个在这个channel上的相应receive,通常这个receive在另一个goroutine。

  • 一个channel上的send happens before这个channel相应receive的完成

var c = make(chan int, 10)

var a string

func f() {

 a = "hello, world"

 c 0

}

func main() {

 go f()

 
 print(a)

}

这个程序保证打印"hello, world"。往a的写happens before c的send,happens before c的相应的receive的完成,happens before print

笔记:这里没解释为什么往a的写happens before c的send,个人理解channel的send前添加了一个内存屏障,保证了其他线程观察到的a的写happens before c的send

  • channel的关闭过程happens before 这个channel上的receive返回零值

笔记:这里很好理解,channel的close操作可以看作send。

  • 在一个unbuffered channel上的receive happens before channel的send完成

笔记:这条有点反直觉


var c = make(chan int)

var a string

func f() {

 a = "hello, world"

 
}


func main() {

 go f()

 c 0

 print(a)

}

这个程序(和上一个类似,除了send和receive语句交换了然后用了一个unbuffered channel)同样保证打印"hello, world"。往a的写happens before c的receive,happens before相应的c上的send的完成,happens before print函数。

如果这个channel是buffered的(比如: c = make(chan int, 1)),那这个程序不会保证打印"hello, world"。(它可能打印空字符串,宕掉,或者做其他事情)

  • 在一个容量C的channel上的第k个receive happens before那个channel上第k C个send的完成

这个规则推广了前一个buffered channel规则。它允许通过buffered channel建模一个计数信号量(semaphore):channel里的item个数对应活跃用户数目,channel的容量对应最大可同时使用量,send item申请(acquire)信号量,receive item释放(release)信号量。这是一个实现有限并发的常见模式。

这个程序为工作列表里的每一个条目开启一个goroutine,但是这些goroutine用了一个有限的channel来确保最多有3个工作在同时运行。


var limit = make(chan int, 3)

func main() {

 for _, w := range work {

 go func(w func()) {

 limit 1

 w()

 
 }(w)

 }

 select{}

}

sync包实现了两种锁数据类型,sync.Mutexsync.RWMutex

对任何sync.Mutex或者sync.RWMutex变量l,假设n < m,调用第n个l.Unlock() happens before第m个l.Lock()返回。

笔记:这个很好理解,锁释放之后才能获取。


var l sync.Mutex

var a string

func f() {

 a = "hello, world"

 l.Unlock() 

}

func main() {

  l.Lock()

 go f()

 l.Lock()

 print(a)

}

这个程序保证打印"hello, world"。第一次调用l.Unlock()(函数f里)happens before第二次调用l.Lock()(函数main里)返回,happens before print

笔记:这里存在和channel同样的问题,没有解释为什么第二次调用l.Lock() happens before print,原因猜测和channel一样l.Lock()会添加一个内存屏障保证happens before关系)

对于任何调用l.RLock在一个sync.RWMutex变量l,有一个这样的n使得l.RLock的返回happens after第n个调用l.Unlock,而且与这个l.RLock配对的l.RUnlock happens before 第 n 1 个l.Lock

笔记:定义比较晦涩,其实描述的规则很简单,读锁在写锁释放后或写锁不存在的条件下才能获取,写锁要在所有存在的读锁释放后才能获取)

Once

sync包提供一种安全的机制应对多个goroutine的并发初始化,即Once类型。多个线程可以执行[once.Do(f](once.Do(f))(这里f是一个函数),但是只有一个goroutine可以执行f(),而其他goroutine的调用会阻塞直到f()返回。

  • 来自[once.Do(f](once.Do(f))的单次对f()调用happens before任何[once.Do(f](once.Do(f))调用的返回

var a string

var once sync.Once

func setup() {

 a = "hello, world"

}

func doprint() {

 [once.Do(setup](once.Do(setup))

 print(a)

}

func twoprint() {

 go doprint()

 go doprint()

}

这个程序调用twoprint将会只调用setup一次。setup函数将会在任何print调用前完成。程序的结果是"hello, world"将会被打印两次。

不正确的同步

注意一个读r可能观测到一个和r并发写w产生的值。即使这种情况发生了,也不能说明happens after r的读将会观测到happens before w的写。


var a, b int

func f() {

 a = 1

 b = 2

}

func g() {

 print(b)

 print(a)

}

func main() {

 go f()

 g()

}

这个程序里可能发生g打印2然后打印0 。

这个事实让一些常见模式无效。

Double Check是一种避免同步开销的方式。举个栗子,下面这个twoprint程序的行为可能不正确:


var a string

var done bool

func setup() {

 a = "hello, world"

 done = true

}

func doprint() {

 if !done {

 [once.Do(setup](once.Do(setup))

 }

 print(a)

}

func twoprint() {

 go doprint()

 go doprint()

}

不能保证观测到done的写入意味着可以观测到a的写入。这个版本可能错误地打印出一个空字符串而不是"hello, world"。

笔记:once只能保证once和f()的happens before关系,但是不能保证once的f()和其他非once的happens before关系)

另外一个不正确的模式是在一个值上的忙等待(busy waiting)


var a string

var done bool

func setup() {

 a = "hello, world"

 done = true

}

func main() {

 go setup()

 for !done {

 }

 print(a)

}

和前一个一样,在main函数里,不能保证观测到done的写入意味着可以观测到a的写入,所以这个程序可能也打印一个空字符串。更糟糕的是,main函数观测到对done的写入也是不能保证的,因为在两个线程之间没有同步。main函数的循环不能保证结束。

对这种情形有一种微妙的变式


type T struct {

 msg string

}

var g *T

func setup() {

 t := new(T)

 t.msg = "hello, world"

 g = t

}

func main() {

 go setup()

 for g == nil {

 }

 print(g.msg)

}

即使main观测到 g != nil然后退出循环,也不能保证它会观测到g.msg的初始化的值。

对于所有这些例子,解决方案都是相同的:使用显式的同步。

后记

go内存模型规范里其实缺失了很重要的一项,对原子操作的规定,在Java里原子操作是可以保证memory order,golang的当前编译器实现也是保证了memory order。不过如果想编写跨编译器,跨编译器版本的兼容代码,安全的建议是在一般go程序中不要依赖原子操作来保证memory order。

引申阅读:

[1].https://go101.org/article/memory-model.html

[2].https://github.com/golang/go/issues/5045


在看多了,优秀的小天同学可能还会给我们出下篇~

到此这篇关于“golang int64 排序_Golang的内存模型”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
golang int64 排序_Golang的内存模型
golang append性能_GoLang定时器实现原理
Go的内存对齐和指针运算详解和实践
golang 结构体断言_Golang中的reflect原理
golang 动态生成函数_GoLang的优点和缺点
golang 反射_golang 内存管理分析
golang key map 所有_golang推断map中指定key是不是存在_后端开发
golang struct数组排序_Golang算法问题之数组按指定规则排序的方法分析
Golang适用的DTO工具
golang map key 正则表达_Golang中的Map

[关闭]