教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 【go】gopl学习笔记(6.基于共享变量的并发)

【go】gopl学习笔记(6.基于共享变量的并发)

发布时间:2021-12-14   编辑:jiaochengji.com
教程集为您提供【go】gopl学习笔记(6.基于共享变量的并发)等资源,欢迎您收藏本站,我们将为您提供最新的【go】gopl学习笔记(6.基于共享变量的并发)资源
<h1>前言</h1>

并发:在有多个goroutine的程序中,每一个goroutine内的语句也是按照既定的顺序去执行的,但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序。当我们不能确认一个事件x是在另一个事件y的前或者后发生的话,就说明x和y这两个事件是并发的。

并发安全:一个函数在线性程序中可以正确地工作,如果在并发的情况下,依然可以正确地工作的话,那么我们就说这个<span style="color:#f33b45;">函数</span>是并发安全的,并发安全的函数不需要额外的同步工作。对于某个<span style="color:#f33b45;">类型</span>来说,如果其所有可访问的方法和操作都是并发安全的话,那么类型便是并发安全的。

<h1>1.竞争条件(Race Conditions)</h1>

什么叫正确地执行呢?其中一种情况是竞争条件,指的是程序在多个goroutine交叉执行操作时,没有给出正确的结果。

数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生,通常表现为更新丢失。

这都是通用地概念,Java并发编程部分都有过涉及,go有哪些方式避免数据竞争情况呢?

<ol><li>不变性:不要去写变量</li><li>不共享:变量限定在单个goroutine A,其余goroutine通过channel让A去更新或查询变量状态</li><li>互斥访问:同一个时刻最多只有一个goroutine在访问</li></ol><h2>1.1 检测</h2>

Go的runtime和工具链为我们提供了一个复杂但好用的动态分析工具,竞争检查器(the race detector)。只要在go build,go run或者go test命令后面加上-race的flag,就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具test,并且会记录下每一个读或者写共享变量的goroutine、所有的同步事件,竞争检查器会检查这些事件,寻找在哪一个goroutine中出现了这样的case,例如其读或者写了一个共享变量,这个共享变量是被另一个goroutine在没有进行干预同步操作便直接写入的。竞争检查器会报告所有的已经发生的数据竞争,但是它只能检测到运行时的竞争条件;并不能证明之后不会发生数据竞争。

<pre><code class="language-bash">go test -run=TestConcurrent -race -v gopl.io/ch9/memo1</code></pre>

 

<blockquote>

不要使用共享数据来通信;使用通信来共享数据

</blockquote> <h1>2.互斥锁</h1>

上一篇文章有讲到使用buffer size固定的channel实现信号量,这里也可以size=1实现二元信号量实现互斥。由于这种场景非常常见,sync包里的Mutex类型已经实现了这个功能,类似于Java的ReentryLock或者synchronized的功能,用法与ReentryLock非常相似,但是go中互斥锁<span style="color:#f33b45;">无法被重入</span>。

<pre><code class="language-Go">var ( mu sync.Mutex // guards balance balance int ) func Deposit(amount int) { mu.Lock() balance = balance amount mu.Unlock() }</code></pre>

在Lock和Unlock之间这个代码段叫做临界区,goroutine可以随便读取或者修改。goroutine在结束后释放锁是必要的,无论以哪条分支都需要Unlock释放,即使是在错误路径中(defer保证释放)。

<pre><code class="language-Go">func Balance() int { mu.Lock() defer mu.Unlock() return balance }</code></pre>

这种函数、互斥锁和变量的编排叫作监视器monitor,类似于Java的synchronized监视器锁

当你使用mutex时,确保mutex和其保护的变量<span style="color:#f33b45;">没有被导出</span>(在go里也就是小写,且不要被大写字母开头的函数访问啦),无论这些变量是包级的变量还是一个struct的字段。

<h2>2.1 不可重入</h2>

为什么go的互斥锁sync.Mutex不可重入呢?

因为在临界区中分步操作时会导致数据的语义不变式被破坏,在锁前释放后会恢复,如果可以重入的话不能保证不变式不被破坏。

不可重入的问题下怎么实现代码复用呢?

<ol><li>分拆函数,</li><li>把可复用的分为小写(未导出)的函数a,该函数不用锁保护;</li><li>导出的多个函数依赖a,且用锁进行保护</li></ol><pre><code class="language-Go">func Withdraw(amount int) bool { mu.Lock() defer mu.Unlock() deposit(-amount) ... return true } func Deposit(amount int) { mu.Lock() defer mu.Unlock() deposit(amount) } // 复用逻辑,不用锁保护,但必须在临界区内调用 func deposit(amount int) { balance = amount }</code></pre> <h1>3.读写锁</h1>

在只有读流量时,并不存在并发修改导致的问题,所以可以允许多个读操作并行执行,Go语言提供的这样的锁是sync.RWMutex,类似于Java的ReentrantReadWriteLock。

<pre><code class="language-Go">var mu sync.RWMutex var balance int func Balance() int { mu.RLock() // 加读锁 defer mu.RUnlock() return balance } func Deposit(amount int) { mu.Lock() // 加写锁 balance = balance amount mu.Unlock() }</code></pre>

RLock只能在临界区共享变量没有任何写入操作时可用。

场景:RWMutex只适用于抢锁的大部分goroutine都是读操作,因为RWMutex需保持复杂内部状态,所以它比互斥锁mutex慢一些。

<h1>4.内存同步-可见性</h1>

Java中有提到过可见性的问题,那是因为cpu内部有高速缓存,对内存的写入一般会在每一个处理器中缓冲,并在必要时一起flush到主存,所以写入操作不一定即时刷新到缓存,读取操作拿到的也不一定是主存最新的数据,而且还有编译器优化等等原因,所以线程A1操作更新完之后不一定能被线程B下一时刻看见。

上文提到的3中规避竞争条件的方式同样能够解决可见性问题,

<ol><li>不变性:不要去写变量</li><li>不共享:变量限定在单个goroutine A,其余goroutine通过channel让A去更新或查询变量状态</li><li>互斥访问:同一个时刻最多只有一个goroutine在访问</li></ol>

Java通过volatile关键字能够轻量级地实现可见性,即每次读取操作都能读取到最新写入的值,go中没有这样的对标。

<h1>5.懒加载sync.Once</h1>

懒加载的情况通常是在使用时判空时进行初始化,非空则直接返回,如下。

<pre><code class="language-Go">func Icon(name string) image.Image { if icons == nil { loadIcons() // one-time initialization 懒加载 } return icons[name] }</code></pre>

但是这种方式是非线程安全的,发生交叉时,可能发生未初始化完成,另一个线程执行获取到不完整的状态。

根据以上篇幅的内容,可以在外层加锁,但是这样严重影响后续获取时的并发性能,由于获取比较多,且之后都不再变,可以考虑读写锁,但是实现较为复杂冗长。幸运的是<span style="color:#f33b45;">sync.Once</span>解决这种一次性初始化的问题。

<pre><code class="language-Go">var loadIconsOnce sync.Once var icons map[string]image.Image // 线程安全 func Icon(name string) image.Image { loadIconsOnce.Do(loadIcons) // 线程互斥地只调用一次 return icons[name] }</code></pre>

每一次对Do(loadIcons)的调用都会定mutex,并会检查boolean变量。在第一次调用时,boolean变量的值是false,Do会调用loadIcons并会将boolean变量设置为true。随后的调用什么都不会做,但是mutex同步会保证loadIcons对内存产生的效果能够对所有goroutine可见。

<h1 id="98-goroutines和线程">6. Goroutines和OS线程</h1>

我们直到Java中的Thread和OS线程是1:1映射的关系,那Go中是什么关系呢?有什么区别呢?

go的线程与OS线程的映射是m:n的,在n个操作系统线程上多工(调度)m个goroutine,GOMAXPROCS变量来决定会有多少个操作系统的线程,即定义上文中的n。默认的值是运行机器上的CPU的核心数。

go上下文中被阻塞或者休眠的goroutine不占用OS线程,I/O中或系统调用中或调用非Go语言函数时,是需要一个对应的操作系统线程。

<pre><code class="language-bash">GOMAXPROCS=1 go run hacker-cliché.go</code></pre> <h2>6.1 区别</h2> <table border="1" cellpadding="1" cellspacing="1"><thead><tr><th>区别维度</th><th>OS线程</th><th>Goroutine</th><th>Java</th></tr></thead><tbody><tr><td>栈</td><td>固定大小(2MB)</td><td>可伸缩(2KB~1GB)</td><td>同OS</td></tr><tr><td>调度</td><td>

OS内核负责调度

中断处理器&scheduler

上下文切换

</td><td>

Go调度器 工作和内核的调度是相似

不需要进入内核的上下文

代价低

</td><td>

映射到内核线程

又OS内核调度

</td></tr><tr><td>线程号</td><td>有</td><td>无</td><td>有</td></tr></tbody></table>

 

到此这篇关于“【go】gopl学习笔记(6.基于共享变量的并发)”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
想系统学习GO语言(Golang
【go】gopl学习笔记(6.基于共享变量的并发)
Golang笔记:语法,并发思想,web开发,Go微服务相关
Go 开发关键技术指南 | 为什么你要选择 Go?(内含超全知识大图)
go 语言学习历程
Golang学习笔记(五):Go语言与C语言的区别
Go 语言十年而立,Go2 蓄势待发
os.create指定路径 golang_Go语言(Golang)环境搭建详解
golang ide 环境搭建_Go语言环境搭建详解(2020版)
想学一门新的编程语言?考虑一下Go (Golang)吧

[关闭]
~ ~