浅谈go的并发编程

目前各种主流的 CPU 频率基本被锁定在了 3Ghz 附近。单核 CPU 的发展的停滞,给多核 CPU 的发展带来了机遇。相应地,编程语言也开始逐步向并行化的方向发展。Go 语言正是在多核和网络化的时代背景下诞生的原生支持并发的编程语言。

来自书《go语言高级编程》

go并发编程和多线程的区别和联系?

回答:常见的并发编程有多种模型,主要包括多线程,消息传递等。

从理论上来说,多线程和基于消息的并发编程(MPI)是等价的,因为多线程的并发模型可以自然的对应多核处理器,主流的操作系统也提供了系统级的多线程支持,

Goroutine和系统级别线程的区别?

Goroutine是 Go 语言特有的并发体,是一种轻量级的线程,由 go 关键字启动。在真实的 Go 语言的实现中,goroutine 和系统线程也不是等价的。两者区别实际上就只是一个量的区别,

  • 系统级线程会有一个固定大小的栈(一般是2MB)这个栈主要来保存函数递归调用的局部变量。系统进程调度主要靠操作系统。
  • Goroutine的创建会以一个很小的栈启动(可能是2KB也可能是4KB),当发生函数递归调用导致栈空间不足的情况下,goroutine会根据动态地伸缩栈的大小(理论可以达到1GB)。goroutine的调度依靠的是go调度器(只有发生阻塞时才会导致调度,采用的是半抢占式的调度)。可以采用rutime.GOMAXPROCS来控制当前正常运行的系统线程的数目
怎样保证原子操作?

首先先了解什么是原子操作:所谓的原子操作指的是并发编程最小不可并行化的操作。说人话就是多个并发体再同一个时间只能有一个对共享资源进行操作。原子操作主要是保证共享资源的完整性

go语言提供几种方法去保证原子操作

  • 互斥锁,sync.Mutex :Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁来保证原子操作。

  • 标准库 sync/atomic提供的的支持。:atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。主要通过原子检测状态来保证原子操作

go是如何保证顺序一致性

其实官网提供了两种方法去解决这个顺序一致性的问题。

  • 互斥锁 sync.Mutex
  • 通道 channel:channel底层实现在src/runtime/chan.go中,内部是一个循环链表

这里重点讲channel

关于channel的接收者和发送者的几种写法
请添加图片描述
我们都清楚,关于channel通信都有相对应的接收者和发送者。正常的主线程为接收者,后台线程为发送者。这个例子是来论证当接收者和发送者颠倒会发送什么样的情况

  • 其中第一种写法是正确的写法。程序打印出“hello, world”

  • 第二种是比较危险的写法。尽管上面的操作是运行的结果相同。因为在mian函数done <- true发送之前。我们可以保证的是全局变量msg已经给赋值了。然后从< - done进行接受信号。最后到main函数进行println。虽然程序打印出“hello, world”。 这样虽然可以但是不推荐原因在下面一点

  • 第三种做法是在第二种的基础上添加了一个缓存。这个时候程序就不会输出 。 main线程的 done <- true 接收操作将不会被后台线程的 <-done 接收操作阻塞,该程序将无法保证打印出“hello, world”。

有缓存通道和无缓存通道的区别?

从上面的例子可以看出。

  • 在无缓存的 Channel 上的每一次发送操作都有与其对应的接收操作相配对。这种类型的 channel 要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。如果两个 goroutine 没有同时准备好,channel 会导致先执行发送或接收操作的 goroutine 阻塞等待。其中任意一个操作都无法离开另一个操作单独存在

  • 对于 Channel 的第 K 个接收完成操作发生在第 K+C 个发送操作完成之前,其中 C 是 Channel 的缓存大小。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。

看到一篇面试题的文章总结很精简:

对于无缓冲区channel:

  • 发送的数据如果没有被接收方接收,那么**发送方阻塞;**如果一直接收不到发送方的数据,接收方阻塞

有缓冲的channel:

  • 发送方在缓冲区满的时候阻塞,接收方不阻塞;接收方在缓冲区为空的时候阻塞,发送方不阻塞。

在这里插入图片描述

https://zhuanlan.zhihu.com/p/471490292

补充:一些不靠谱的代码写法

原子操作为线程同步敏感资源管理提供了一些保障。不过这个保障的前提是顺序一致性的内存模型。说人话就是主线程如何去监听goroutine是否执行完毕再结束。

一个比较简单的方法。 我定义一个全局变量。主函数去监听这个变量,如下:

var a string
var done bool

func haha() {
    a = "hello, world"
    done = true
}

func main() {
    go haha()
    for !done {}
    print(a)
}

我们创建一个线程。对字符串进行赋值操作。主线程进行循环的监听这个去全局变量。只要done == True的时候就会往下执行。不然就会一直卡着。其实这种操作是非常的危险。

因为go语言不能保证mian函数对done的写入是在a赋值之前还是之后。因此这个程序可能会打印一个空字符串

在 Go 语言中,同一个 Goroutine 线程内部,顺序一致性内存模型是得到保证的但是不同的 Goroutine 之间,并不满足顺序一致性内存模型,需要通过明确定义的同步事件来作为同步的参考。如果两个事件不可排序,那么就说这两个事件是并发的。为了最大化并行,Go 语言的编译器和处理器在不影响上述规定的前提下可能会对执行语句重新排序(CPU 也会对一些指令进行乱序执行)。

还有一些不靠谱的操作,例如使用sleep来保证顺序的一致性

func main() {
    go println("hello, world")
    time.Sleep(time.Second)
}

主线程消息的一秒钟。这个程序已经大概率正常的输出了。有些人会觉得这个程序是没有毛病的,其实其是不稳健的。依然是有失败的可能的。

代码参考 网址:https://github.com/chai2010/advanced-go-programming-book

本文为阅读书《go语言高级编程》一书写下的一些关于内存模型的并发编程而有感写。侵删。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值