Go语言是
基于消息并发模型的集大成者,它将基于CSP模型的并发编程内置到了语言中,通
过一个go关键字就可以轻易地启动一个Goroutine,与Erlang不同的是Go语言的
Goroutine之间是共享内存的。

Goroutine和系统线程

Goroutine是Go语言特有的并发体,是一种轻量级的线程,由go关键字启动。在真
实的Go语言的实现中,goroutine和系统线程也不是等价的。尽管两者的区别实际
上只是一个量的区别,但正是这个量变引发了Go语言并发编程质的飞跃。

系统线程

每个系统级线程都会有一个固定大小的栈(一般默认可能是2MB),这个栈
主要用来保存函数递归调用时参数和局部变量。

固定了栈的大小导致了两个问题:
一是对于很多只需要很小的栈空间的线程来说是一个巨大的浪费,二是对于少数需
要巨大栈空间的线程来说又面临栈溢出的风险。针对这两个问题的解决方案是:要
么降低固定的栈大小,提升空间的利用率;要么增大栈的大小以允许更深的函数递
归调用,但这两者是没法同时兼得的。

Goroutine

相反,一个Goroutine会以一个很小的栈启动

(可能是2KB或4KB),当遇到深度递归导致当前栈空间不足时,Goroutine会根据
需要动态地伸缩栈的大小(主流实现中栈的最大值可达到1GB)。

因为启动的代价
很小,所以我们可以轻易地启动成千上万个Goroutine。

Go的运行时还包含了其自己的调度器,这个调度器使用了一些技术手段,可以在n
个操作系统线程上多工调度m个Goroutine。Go调度器的工作和内核的调度是相似
的,但是这个调度器只关注单独的Go程序中的Goroutine。Goroutine采用的是半抢
占式的协作调度,只有在当前Goroutine发生阻塞时才会导致调度;同时发生在用户
态,调度器会根据具体函数只保存必要的寄存器,切换的代价要比系统线程低得
多。运行时有一个 runtime.GOMAXPROCS 变量,用于控制当前运行正常非阻塞
Goroutine的系统线程数目。

在Go语言中启动一个Goroutine不仅和调用函数一样简单,而且Goroutine之间调度
代价也很低,这些因素极大地促进了并发编程的流行和发展。

原子操作

所谓的原子操作就是并发编程中“最小的且不可并行化”的操作。通常,如果多个并
发体对同一个共享资源进行的操作是原子的话,那么同一时刻最多只能有一个并发
体对该资源进行操作。从线程角度看,在当前线程修改共享资源期间,其它的线程
是不能访问该资源的。原子操作对于多线程并发编程模型来说,不会发生有别于单
线程的意外情况,共享资源的完整性可以得到保证。

一般情况下,原子操作都是通过“互斥”访问来保证的,通常由特殊的CPU指令提供
保护。当然,如果仅仅是想模拟下粗粒度的原子操作,我们可以借助
于 sync.Mutex 来实现:

package main

import (
"fmt"
"sync"
) var total struct {
sync.Mutex
value int
} func worker(wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i <= 100; i++ {
total.Lock()
total.value += i
total.Unlock()
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go worker(&wg)
go worker(&wg)
wg.Wait()
fmt.Println(total.value)
}

  

在 worker 的循环中,为了保证 total.value += i 的原子性,我们通
过 sync.Mutex 加锁和解锁来保证该语句在同一时刻只被一个线程访问。对于多
线程模型的程序而言,进出临界区前后进行加锁和解锁都是必须的。如果没有锁的
保护, total 的最终值将由于多线程之间的竞争而可能会不正确。

用互斥锁来保护一个数值型的共享资源,麻烦且效率低下。标准库
的 sync/atomic 包对原子操作提供了丰富的支持。我们可以重新实现上面的例
子:

package main

import (
"fmt"
"sync"
"sync/atomic"
) var total uint64 func worker(wg *sync.WaitGroup) {
defer wg.Done()
var i uint64
for i = 0; i <= 100; i++ {
atomic.AddUint64(&total, i)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go worker(&wg)
go worker(&wg)
wg.Wait()
fmt.Println(total) }

 

atomic.AddUint64 函数调用保证了 total 的读取、更新和保存是一个原子操
作,因此在多线程中访问也是安全的。

原子操作配合互斥锁可以实现非常高效的单件模式。互斥锁的代价比普通整数的原
子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标
志位状态降低互斥锁的使用次数来提高性能。

顺序一致性内存模型

package main

var a string
var done bool func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}

  

我们创建了 setup 线程,用于对字符串 a 的初始化工作,初始化完成之后设
置 done 标志为 true 。 main 函数所在的主线程中,通过 for !done {} 检
测 done 变为 true 时,认为字符串初始化工作完成,然后进行字符串的打印工
作。

但是Go语言并不保证在 main 函数中观测到的对 done 的写入操作发生在对字符
串 a 的写入的操作之后,因此程序很可能打印一个空字符串。更糟糕的是,因为
两个线程之间没有同步事件, setup 线程对 done 的写入操作甚至无法
被 main 线程看到, main 函数有可能陷入死循环中。

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

因此,如果在一个Goroutine中顺序执行 a = 1; b = 2; 两个语句,虽然在当前的
Goroutine中可以认为 a = 1; 语句先于 b = 2; 语句执行,但是在另一个
Goroutine中 b = 2; 语句可能会先于 a = 1; 语句执行,甚至在另一个Goroutine
中无法看到它们的变化(可能始终在寄存器中)。也就是说在另一个Goroutine看
来, a = 1; b = 2; 两个语句的执行顺序是不确定的。如果一个并发程序无法确
定事件的顺序关系,那么程序的运行结果往往会有不确定的结果。比如下面这个程

package main

func main() {
    go println("你好, 世界")
}

  

根据Go语言规范, main 函数退出时程序结束,不会等待任何后台线程。因为
Goroutine的执行和 main 函数的返回事件是并发的,谁都有可能先发生,所以什
么时候打印,能否打印都是未知的。

 

用前面的原子操作并不能解决问题,因为我们无法确定两个原子操作之间的顺序。
解决问题的办法就是通过同步原语来给两个事件明确排序:

package main

func main() {
done := make(chan int)
go func() {
println("你好, 世界")
done <- 1
}()
<-done
}

  

当 <-done 执行时,必然要求 done <- 1 也已经执行。根据同一个Gorouine依然
满足顺序一致性规则,我们可以判断当 done <- 1 执行时, println("你好, 世
界") 语句必然已经执行完成了。因此,现在的程序确保可以正常打印结果。

当然,通过 sync.Mutex 互斥量也是可以实现同步的:

package main

import "sync"

func main() {
var mu sync.Mutex
mu.Lock()
go func() {
println("你好, 世界")
mu.Unlock()
}()
mu.Lock()
}

  

可以确定后台线程的 mu.Unlock() 必然在 println("你好, 世界") 完成后发生
(同一个线程满足顺序一致性), main 函数的第二个 mu.Lock() 必然在后台线
程的 mu.Unlock() 之后发生( sync.Mutex 保证),此时后台线程的打印工作已
经顺利完成了。

golang学习笔记 ---面向并发的内存模型的更多相关文章

  1. Golang面向并发的内存模型

    Import Advanced Go Programming 1.5 面向并发的内存模型 在早期,CPU都是以单核的形式顺序执行机器指令.Go语言的祖先C语言正是这种顺序编程语言的代表.顺序编程语言中 ...

  2. Python Web学习笔记之并发编程IO模型

    了解新知识之前需要知道的一些知识 同步(synchronous):一个进程在执行某个任务时,另外一个进程必须等待其执行完毕,才能继续执行 #所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调 ...

  3. golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题

    golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题 下面这个程序运行的能num结果是什么? package main import ( "fmt" " ...

  4. go语言,golang学习笔记2 web框架选择

    go语言,golang学习笔记2 web框架选择 用什么go web框架比较好呢?能不能推荐个中文资料多的web框架呢? beego框架用的人最多,中文资料最多 首页 - beego: 简约 & ...

  5. JVM学习笔记(四)------内存调优【转】

    转自:http://blog.csdn.net/cutesource/article/details/5907418 版权声明:本文为博主原创文章,未经博主允许不得转载. 首先需要注意的是在对JVM内 ...

  6. JVM学习笔记(四)------内存调优

    首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提 ...

  7. golang学习笔记10 beego api 用jwt验证auth2 token 获取解码信息

    golang学习笔记10 beego api 用jwt验证auth2 token 获取解码信息 Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放 ...

  8. golang学习笔记8 beego参数配置 打包linux命令

    golang学习笔记8 beego参数配置 打包linux命令 参数配置 - beego: 简约 & 强大并存的 Go 应用框架https://beego.me/docs/mvc/contro ...

  9. golang学习笔记6 beego项目路由设置

    golang学习笔记5 beego项目路由设置 前面我们已经创建了 beego 项目,而且我们也看到它已经运行起来了,那么是如何运行起来的呢?让我们从入口文件先分析起来吧: package main ...

随机推荐

  1. 【Nodejs】外研社小学英语教材一年级起各年级英语音频下载(全)

    两个爬虫合作成的,放在这里分享给有需要的人. 下载地址:https://pan.baidu.com/s/19hJ59SsX9uTfFtBxaRxJ8A 2018年5月10日

  2. 在CentOS 7上安装Nginx服务器

    下面我就我在CentOS上安装Nginx经验做简单的记录,以备后查. 1.下载nginx-release包 以CentOS 7为例,下载nginx软件包:http://nginx.org/packag ...

  3. 让.aspx同样实现.ashx文件的功能: IHttpHandler

    我们需要一个能够调用该处理程序的入口点.在此上下文中,该处理程序代码的入口点只不过是一个HTTP终点——即,一个公共的URL.该URL必须有一个惟一的名称,使IIS和ASP.NET运行库能够把它映射到 ...

  4. Go语言中异常处理painc()和recover()的用法

    Go语言中异常处理painc()和recover()的用法 1.Painc用法是:用于抛出错误.Recover()用法是:将Recover()写在defer中,并且在可能发生panic的地方之前,先调 ...

  5. 斯坦福《机器学习》Lesson1-3感想-------3、线性回归二

    从上一篇可知.在监督学习里最重要的就是确定假想函数h(θ),即通过使得代价函数J(θ)最小,从而确定h(θ). 上一篇通过梯度下降法求得J(θ)最小,这篇我们将使用矩阵的方法来解释. 1.普通最小二乘 ...

  6. http 请求报文

    1.报文 2.http请求方法 restful接口 post:创建 put:更新

  7. vue refs v-for 使用注意

    当 v-for 用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组. https://cn.vuejs.org/v2/api/#ref https://www.w3cplus.com ...

  8. linux下切换python2和python3(转)

    0x00 为什么需要有两个版本的Python Python2和Python3不兼容是每个接触过Python的开发者都知道的事,虽说Python3是未来,但是仍然有很多项目采用Python2开发.Lin ...

  9. 一条SQL语句获取具有父子关系的分类列表(mysql)

    有如下表数据: 获取“菜单”分类的子分类数据列表: SELECT a.cat_id, a.cat_name, a.sort_order AS parent_order, a.cat_id, b.cat ...

  10. 【DB2】表函数监控数据库

    1.快照表函数 在DB2 V9中能够使用SQL表函数捕获快照,以下是部分表函数列表: 快照表函数                           返回的信息 SNAPSHOT_DBM        ...