Go面向并发的内存模型
  Eo7GKm3ms3Sh 2023年11月02日 38 0

 

1.闲聊

在远古时期cpu都是以单核的形式执行机器的指令。随着科技和时代的进步,单核无法满足人类日益贪婪的需求,于是多核cpu应用而生。编程语言也不甘示弱,开始利用多核cpu的优势逐渐走向了并行的方向。

Go语言在出生的时候,就已经是多核时代的天下了。于是那群牛逼的Go语言之父呢,就结合了多门语言的特性,创造了Go语言自身的并发机制。这也是为什么Go语言有着“天生高并发“的称号。

2.内存模型

2.1 什么是内存模型

在多核多线程的背景下,多个不同的cpu是如何以一种统一的形式来与内存进行交互的。

2.2 内存模型有哪些?

多线程、消息传递、顺序一致性内存模型等

3. 回归

这篇文章是写Go的并发的内存模型,不讲其他的(因为也不懂)

聊Go的并发离不开Goroutine。

4. 什么是Goroutine

Goroutine是go语言独有的并发体。名字叫协程,其实就是一种轻量级的线程。我们都知道每个线程都有一个固定的大小栈,一般默认为2MB,Goroutine呢也有大小,她的大小栈呢默认为2KB或4KB,在内存空间高昂的年代,小就是牛逼。

线程呢固定了栈的小大产生了两个问题:

  1. 对于很多只需要很小的栈空间的线程来说是一个巨大的浪费
  2. 对于少数需要巨大栈空间的线程来说又面临栈溢出的风险

如何解决:

要么降低固定的栈大小,提升空间的利用率;要么增大栈的大小以允许更深的函数递归调用,但这两者是没法同时兼得

于是呼Goroutine站了出来。上面讲到了Goroutine的栈大小默认为2KB或4KB,所以她启动的时候占用很小的空间资源,但是当遇到深度递归导致当前栈空间不足时,Goroutine会根据需要动态的伸缩栈的大小(主流实现中栈的最大值可达到1GB). 有一种你线程解决不了,我解决了!一副傲视群雄的样子。Goroutine理论上可以启动成千上万个。这么牛逼,如何启动呢,她只需要一个 go 关键字即可启动。

Go呢自带调度器,可以调度Goroutine。调度器呢属于半抢半占的形式进行调度的,当当前的Goroutine发生阻塞时,才会导致调度。(有点类似于男人之间的对话,你行不行 不行我来的意思)

闲话不多话,遭多也没了~

5. 原子操作

出现这个问题的场景是在并发的情况下,出现了多个并发体对同一个共享资源数据竞争的问题。如何解决呢。

5.1 加锁

可以通过 互斥锁 实。Go 语言提供了两个包,分别为sync.Mutex和sync.RWMutex,在对同一资源操作的时候比如更新、删除、读等,对当前的Goroutine进行加锁。

package main

import (
	"fmt"
	"sync"
)

var (
	m     sync.Mutex
	v int
)

func do(wg *sync.WaitGroup) {
	defer wg.Done()

	for i := 0; i <= 100; i++ {
		m.Lock()
		v++
		m.Unlock()
	}
}

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	go do(&wg)
	go do(&wg)
	wg.Wait()

	fmt.Println(v)
}
5.2 sync/atomic包

加锁在并发量和数据量大的情况下,会导致一个性能下载的问题。那么Go语言提供了sync/atomic包,内置了对一个数值型的共享资源的原子操作。简直不要太好~

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var score uint64

func do(wg *sync.WaitGroup) {
	defer wg.Done()

	var i uint64
	for i = 0; i <= 100; i++ {
		atomic.AddUint64(&score, i)
	}
}

func main() {
	var wg sync.WaitGroup
	wg.Add(2)

	go do(&wg)
	go do(&wg)

	wg.Wait()
	fmt.Println(score)
}

如果你用过sync.Once 实现的单列模式,会发现它也是使用atomic实现的。

// A Once must not be copied after first use.
type Once struct {
	// done indicates whether the action has been performed.
	// It is first in the struct because it is used in the hot path.
	// The hot path is inlined at every call site.
	// Placing done first allows more compact instructions on some architectures (amd64/386),
	// and fewer instructions (to calculate offset) on other architectures.
	done uint32
	m    Mutex
}

// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
// 	var once Once
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation. A new instance of
// Once is required for each function to execute.
//
// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
// 	config.once.Do(func() { config.init(filename) })
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
//
func (o *Once) Do(f func()) {
	// Note: Here is an incorrect implementation of Do:
	//
	//	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
	//		f()
	//	}
	//
	// Do guarantees that when it returns, f has finished.
	// This implementation would not implement that guarantee:
	// given two simultaneous calls, the winner of the cas would
	// call f, and the second would return immediately, without
	// waiting for the first's call to f to complete.
	// This is why the slow path falls back to a mutex, and why
	// the atomic.StoreUint32 must be delayed until after f returns.

	if atomic.LoadUint32(&o.done) == 0 {
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}

6 顺序一致性内存模型

Goroutine 是以一种异步的形式执行的。如何保证多个Goroutine按顺序执行呢?

看下面代码

package main

func main() {
	go func() {
		println("我会执行吗?")
	}()
}

这段代码会打印出来这句话吗?

写Go的小伙伴都知道,main函数其实也是一个Goroutine,当main所在的Goroutine执行完之后,就会执行os.Exit(1)退出当前Goroutine,main退出了,那句也就无法打印出来了。

如何实现 打印出 我会执行吗?这句话呢?

方法呢有多种,一一来实现,可能不全,望指正~

6.1 第一种 for 阻塞

有一点不太友好的地方,会一直阻塞下去,直到天荒地老~

package main

func main() {
	go func() {
		println("我会执行吗?")
	}()
	for {

	}
}
6.2 第二种 加锁
package main

import "sync"

func main() {
	var wg sync.Mutex

	wg.Lock()
	go func() {
		println("我会执行吗?")
		wg.Unlock()
	}()
	wg.Lock()
	println("我肯定执行的...")
}
6.3 第三种 通道

Go语言内置channel 分为两种,一种是无缓冲的通道和有缓冲的通道。无缓冲的通道,在从通道接受前一定会先执行往通道里发送。其实可以理解为无缓冲的通道是同步的。通道更详细的理解自行查看相关文档

采用无缓冲的通道来实现

package main

func main() {
	c := make(chan int)

	go func() {
		println("我会执行吗?")
		c <- 1
	}()
	<-c
}
6.4 第四种

Go语言内置了sync.WaitGroup包 等带一个Goroutine执行完,执行下一个Goroutine。代码实现

package main

import "sync"

func main() {
	var s sync.WaitGroup

	s.Add(1)
	go func() {
		println("我会执行吗?")
		s.Done()
	}()
	s.Wait()
}

以上就是本篇内容。


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  KRe60ogUm4le   2024年03月22日   39   0   0 linux算法
  KRe60ogUm4le   24天前   17   0   0 递归算法
Eo7GKm3ms3Sh
最新推荐 更多

2024-05-03