go并发编程之美(二)、go内存模型_go 并发内存模型(1)

func f() {
fmt.Print(a)
wg.Done()
}

func hello() {
a = “hello, world”
go f()
}
func main() {
wg.Add(1)

hello()
wg.Wait()

}

如上代码调用hello方法后肯定会输出”hello,world”,可能等hello方法执行完毕后才输出(由于调度的原因)。

3.3 销毁goroutine(Goroutine destruction)

一个goroutine的销毁操作并不能确保 happen before 程序中的任何事件,比如下面例子:

var a string

func hello() {
go func() { a = “hello” }()
print(a)
}

如上代码 goroutine内对变量a的赋值并没有加任何同步措施,所以并能不保证hello函数所在的goroutine对变量a的赋值可见。如果要确保一个goroutine对变量的修改对其他goroutine可见,必须使用一定的同步机制,比如锁、通道来建立对同一个变量读写的偏序关系

3.4 通道通信(Channel communication)

在go中通道是用来解决多个goroutines之间进行同步的主要措施,在多个goroutines中,每个对通道进行写操作的goroutine都对应着一个从通道读操作的goroutine。

3.4.1 有缓冲通道

在有缓冲的通道时候向通道写入一个数据总是 happen before 这个数据被从通道中读取完成,如下例子:

package main

import (
“fmt”
)

var c = make(chan int, 10)
var a string

func f() {
a = “hello, world” //1
c <- 0 //2
}

func main() {
go f() //3
<-c //4
fmt.Print(a) //5
}

如上代码运行后可以确保输出”hello, world”,这里对变量a的写操作(1) happen before 向通道写入数据的操作(2),而向通道写入数据的操作(2)happen before 从通道读取数据完成的操作(4),而步骤(4)happen before 步骤(5)的打印输出。

另外关闭通道的操作 happen before 从通道接受0值(关闭通道后会向通道发送一个0值),修改上面代码(2)如下:

package main

import (
“fmt”
)

var c = make(chan int, 10)
var a string

func f() {
a = “hello, world” //1
close© //2
}

func main() {
go f() //3
<-c //4
fmt.Print(a) //5
}

然后在运行也可以确保输出”hello, world”。

注:在有缓冲通道中通过向通道写入一个数据总是 happen before 这个数据被从通道中读取完成,这个happen before规则使多个goroutine中对共享变量的并发访问变成了可预见的串行化操作。

3.4.2 无缓冲通道

对应无缓冲的通道来说从通道接受(获取叫做读取)元素 happen before 向通道发送(写入)数据完成,看下下面代码:

package main

import (
“fmt”
)

var c = make(chan int)
var a string

func f() {
a = “hello, world” //1
<-c //2
}

func main() {
go f() //3
c <- 0 //4
fmt.Print(a) //5
}

如上代码运行也可保证输出”hello, world”,注意改程序相比上一个片段,通道改为了无缓冲,并向通道发送数据与读取数据的步骤(2)(4)调换了位置。

在这里写入变量a的操作(1)happen before 从通道读取数据完毕的操作(2),而从通道读取数据的操作 happen before 向通道写入数据完毕的操作(4),而步骤(4) happen before 打印输出步骤(5)。

注:在无缓冲通道中从通道读取数据的操作 happen before 向通道写入数据完毕的操作,这个happen before规则使多个goroutine中对共享变量的并发访问变成了可预见的串行化操作。

如上代码如果换成有缓冲的通道,比如c = make(chan int, 1)则就不能保证一定会输出”hello, world”。

3.4.3 规则抽象

从容量为C的通道接受第K个元素 happen before 向通道第k+C次写入完成,比如从容量为1的通道接受第3个元素 happen before 向通道第3+1次写入完成。

这个规则对有缓冲通道和无缓冲通道的情况都适用,有缓冲的通道可以实现信号量计数的功能,比如通道的容量可以认为是最大信号量的个数,通道内当前元素个数可以认为是剩余的信号量个数,向通道写入(发送)一个元素可以认为是获取一个信号量,从通道读取(接受)一个元素可以认为是释放一个信号量,所以有缓冲的通道可以作为限制并发数的一个通用手段:

package main

import (
“fmt”
“time”
)

var limit = make(chan int, 3)

func sayHello(index int){
fmt.Println(index )
}

var work []func(int)
func main() {

work := append(work,sayHello,sayHello,sayHello,sayHello,sayHello,sayHello)

for i, w := range work {
go func(w func(int),index int) {
limit <- 1
w(index)
<-limit
}(w,i)
}

time.Sleep(time.Second * 10)
}

如上代码main goroutine里面为work列表里面的每个方法的执行开启了一个单独的goroutine,这里有6个方法,正常情况下这7个goroutine可以并发运行,但是本程序使用缓存大小为3的通道来做并发控制,导致同时只有3个goroutine可以并发运行。

四、锁(locks)

sync包实现了两个锁类型,分别为 sync.Mutex(互斥锁)和 sync.RWMutex(读写锁)。

对应任何sync.Mutex or sync.RWMutex类型的遍历I来说调用n次 l.Unlock() 操作 happen before 调用m次l.Lock()操作返回,其中n<m,我们看下面程序:

package main

import (
“fmt”
“sync”
)

var l sync.Mutex
var a string

func f() {
a = “hello, world” //1
l.Unlock() //2
}

func main() {
l.Lock() //3
go f() //4
l.Lock() //5
fmt.Print(a) //6
}

运行上面代码可以确保输出”hello, world”,其中对变量a的赋值操作(1) happen before 步骤(2),第一次调用 l.Unlock()的操作(2) happen before 第二次调用l.Lock()的操作(5),操作(5) happen before 打印输出操作(6)

另外对任何一个sync.RWMutex类型的变量l来说,存在一个次数n,调用 l.RLock操作happens after 调用n次 l. Unlock(释放写锁)并且相应的 l.RUnlock happen before 调用n+1次 l.Lock(写锁)

package main

import (
“fmt”
“sync”
)

var l sync.RWMutex
var a string

func unlock() {
a = “unlock” //1
l.Unlock() //2
}

func runlock() {
a = “runlock” //3
l.RUnlock() //4
}

func main() {
l.Lock() //5
go unlock() //6

l.RLock() //7
fmt.Println(a) //8
go runlock() //9

l.Lock() //10
fmt.Print(a) //11
l.Unlock()
}

  • 运行上面代码一定会输出如下:

unlock
runlock

  • 如上代码 (1)对a的赋值 happen before 代码(2),而对l.RLock() (代码7) 的调用happen after对l.Unlock()(代码2)的第1次调用,所以代码(8)输出unlock。
  • 而对代码(7)l.RLock() 的调用happen after对l.Unlock()(代码2) 的第1次调用,相应的有对l.RUnlock() (代码4)的调用happen before 第2次对l.Lock()(代码4)的调用,所以代码(11)输出runlock

也就是这里对任何一个sync.RWMutex类型的变量l来说,存在一个次数1,调用 l.RLock操作happens after 调用1次 l. Unlock(释放写锁)并且相应的 l.RUnlock happen before 调用2次 l.Lock(写锁)

五、一次执行(Once)

sync包提供了在多个goroutine存在的情况下进行安全初始化的一种机制,这个机制也就是提供的Once类型。多(goroutine)下多个goroutine可以同时执行once.Do(f)方法,其中f是一个函数,但是同时只有一个goroutine可以真正运行传递的f函数,其他的goroutine则会阻塞直到运行f的goroutine运行f完毕。

多goroutine下同时调用once.Do(f)时候,真正执行f()函数的goroutine, happen before 任何其他由于调用once.Do(f)而被阻塞的goroutine返回:

package main

import (
“fmt”
“sync”
“time”
)

var a string
var once sync.Once
var wg sync.WaitGroup

func setup() {
time.Sleep(time.Second * 2) //1
a = “hello, world”
fmt.Println(“setup over”) //2
}

func doprint() {
once.Do(setup) //3
fmt.Println(a) //4
wg.Done()
}

func twoprint() {
go doprint()
go doprint()
}

func main() {
wg.Add(2)
twoprint()

wg.Wait()
}

如上代码运行会输出:
setup over
hello, world
hello, world

  • 上面代码使用wg sync.WaitGroup等待两个goroutine运行完毕,由于 setup over只输出一次,所以setup方法只运行了一次
  • 由于输出了两次hello, world说明当一个goroutine在执行setup方法时候,另外一个在阻塞。

六、不正确的同步(Incorrect synchronization)

6.1 不正确的同步案例(一)

需要注意的是虽然一个goroutine对一个变量的读取操作r,可以观察到另外一个goroutine的写操作w对变量的修改,但是这不意味着happening after 读操作r的读操作可以看到 happen before写操作w的写操作对变量的修改(需要注意这里的先后指的是代码里面声明的操作的先后顺序,而不是实际执行时候的):

var a, b int

func f() {
a = 1//1
b = 2//2
}

func g() {
print(b)//3
print(a)//4
}

func main() {
go f()//5
g()//6
}

  • 比如上面代码一个可能的输出为先打印2,然后打印0
  • 由于代码(1)(2)没有有任何同步措施,所以经过重排序后可能先执行代码(2),然后执行代码(1)。
  • 另外由于步骤(5)开启了一个新goroutine来执行f函数,所以f函数和g函数是并发运行,并且两个goroutine没做任何同步。
  • 假设f函数先执行,并且由于重排序限制性了步骤(2),然后g函数执行了步骤(3)则这时候会打印出2,然后执行步骤(4)则打印出0,然后执行步骤(1)给变量a赋值。

也就是说这里即使假设步骤(3)的读取操作r 对步骤(2)的写操作w的内容可见,但是还没不能保证步骤(3)的读取操作后面的读取操作步骤(4)可以看到 先于代码中声明的在步骤(2)前面的代码(1)对变量a赋值的内容。

6.2 不正确的同步案例(二)

使用双重检查机制来避免使用同步带来的开销,如下代码:

var a string
var done bool

func setup() {
a = “hello, world”

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

9538356)]
[外链图片转存中…(img-MjVbfDZe-1712969538356)]
[外链图片转存中…(img-9GpY1nTW-1712969538357)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-8culCpEy-1712969538357)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 13
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值