跟着 GPT-4 从0到1学习 Golang 并发机制(三)
一、前言
话接上回《跟着 GPT-4 从0到1学习 Golang 并发机制(二)》,我想试试看 GPT-4 能不能当好一个编程老师,循循善诱,由浅入深,深入浅出…… 给我讲解下 Golang 里的并发机制。
今天继续当白纸,GPT 为笔,代码为颜料,挥笔泼墨,接着作画。
二、开聊
下文通过 DevChat 和 GPT-4 交互,如果你对 DevChat 感兴趣,可以跳转下文浏览:
2.1 关于 goroutine 泄露问题
- 我问 GPT:
可以介绍下 goroutine 泄露问题吗
- GPT 说:
Goroutine 泄露是 Go 程序中一个常见的问题,它发生在你启动了一个 goroutine,但是没有提供一个结束它的方式。这个 goroutine 就会一直运行,占用内存和 CPU,直到整个程序结束。如果你的程序启动了大量的这样的 goroutine,它可能会消耗大量的资源,甚至导致程序崩溃。
以下是一个 goroutine 泄露的示例:
package main import (
"fmt"
"time"
) func leakyFunction() {
go func() {
for {
fmt.Println("I'm leaking!")
time.Sleep(1 * time.Second)
}
}()
} func main() {
leakyFunction()
time.Sleep(5 * time.Second)
}
在这个示例中,
leakyFunction启动了一个 goroutine,这个 goroutine 会无限循环打印一条消息。然而,leakyFunction并没有提供一个结束这个 goroutine 的方式,所以这个 goroutine 会一直运行,直到整个程序结束。为了避免 goroutine 泄露,你应该总是提供一个结束 goroutine 的方式。这通常可以通过 context 取消、关闭 channel 或者使用
sync.WaitGroup来实现。以下是一个修改后的示例,避免了 goroutine 泄露:
package main import (
"context"
"fmt"
"time"
) func notLeakyFunction(ctx context.Context) {
go func() {
for {
select {
case <-ctx.Done():
return
default:
fmt.Println("I'm not leaking!")
time.Sleep(1 * time.Second)
}
}
}()
} func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
notLeakyFunction(ctx)
time.Sleep(10 * time.Second)
}
在这个示例中,我们使用了一个可取消的 context。当 context 被取消时,goroutine 会检测到这个信号,并结束运行。这样,我们就避免了 goroutine 泄露。
寥寥几句,似乎已经讲完了这个话题。好吧,那继续看内存模型去。
2.2 内存模型
- 我又问 GPT:
继续介绍下 Go 的内存模型吧
- GPT 说:
Go 的内存模型定义了在并发程序中,如何以及何时对变量的读写操作对其他 goroutine 可见。它是理解并发编程中数据同步的关键。
在 Go 中,如果一个 goroutine 在没有使用 channel 或者其他同步原语(如
sync包中的锁)的情况下,对一个变量的写入操作对其他 goroutine 是不可见的。这是因为 Go 运行时并不保证在没有同步的情况下,一个 goroutine 的写入操作何时对其他 goroutine 可见。以下是一个示例:
package main import (
"fmt"
"time"
) var a string
var done bool func setup() {
a = "hello, world"
done = true
} func main() {
go setup()
time.Sleep(1 * time.Second)
if done {
fmt.Println(a)
}
}
在这个示例中,
setupgoroutine 对a和done的写入操作可能对maingoroutine 不可见,即使我们在setup和main之间添加了time.Sleep。这是因为 Go 的内存模型并不保证在没有同步的情况下,一个 goroutine 的写入操作何时对其他 goroutine 可见。为了在 goroutine 之间正确同步数据,你应该使用 channel 或者
sync包中的原语。例如,你可以使用sync.Mutex来保护对共享变量的访问,或者使用sync.WaitGroup来等待一个 goroutine 完成。以下是一个修改后的示例,使用了
sync包来同步数据:package main import (
"fmt"
"sync"
) var a string
var done bool
var mu sync.Mutex func setup() {
a = "hello, world"
mu.Lock()
done = true
mu.Unlock()
} func main() {
go setup()
time.Sleep(1 * time.Second)
mu.Lock()
if done {
fmt.Println(a)
}
mu.Unlock()
}
在这个示例中,我们使用了
sync.Mutex来保护对done的访问。这样,我们就可以确保setupgoroutine 对a和done的写入操作对maingoroutine 可见。
这个结果看起来并不好,我不满意。
- 我接着问 GPT:
这个例子其实并不能太好地解释“内存模型”,因为不管执行多少次,main 函数中都会读到 done 为 true。我希望你更深入地介绍 happens before 以及 Golang 哲学中的“Do not communicate by sharing memory; instead, share memory by communicating”
- GPT 说:
你的观察是正确的,我之前的例子可能并不能很好地展示 Go 的内存模型。让我们更深入地讨论一下。
Happens Before
在 Go 的内存模型中,"happens before" 是一个非常重要的概念。如果我们说事件 A happens before 事件 B,那么意味着在所有的 goroutine 中,事件 A 的效果对事件 B 是可见的。也就是说,如果一个 goroutine 对一个变量的写入操作 happens before 另一个 goroutine 的读取操作,那么读取操作将会看到写入操作的效果。
在 Go 中,以下几种情况会建立 happens before 关系:
- 对同一个 channel 的发送操作 happens before 相应的接收操作。
- 对一个 Mutex 或者 RWMutex 的解锁操作 happens before 后续的锁定操作。
- 对一个 WaitGroup 的 Add 操作 happens before 相应的 Wait 操作。
- 对一个 Cond 的 Signal 或者 Broadcast 操作 happens before 相应的 Wait 操作返回。
"Do not communicate by sharing memory; instead, share memory by communicating"
这是 Go 并发哲学的核心原则。它鼓励我们使用 channel 来在 goroutine 之间传递数据,而不是通过共享内存。这样,我们可以通过数据的所有权转移来避免数据竞争。
以下是一个使用 channel 来在 goroutine 之间传递数据的示例:
package main import (
"fmt"
"time"
) func worker(done chan bool) {
fmt.Print("working...")
time.Sleep(time.Second)
fmt.Println("done") done <- true
} func main() {
done := make(chan bool, 1)
go worker(done) <-done
}
在这个示例中,我们创建了一个 worker goroutine,并给它传递了一个 channel。当 worker 完成工作后,它会向这个 channel 发送一个信号。主 goroutine 会等待这个信号,然后结束程序。这样,我们就通过 channel 在 goroutine 之间传递了数据,而不是通过共享内存。
- 我接着问 GPT:
看了这个新的例子,我大概理解“happens before”以及“share memory by communicating”了。在前面一个例子中,你通过共享内存来实现 goroutine 之间的通信,其实是不可靠的,因为读取操作可以发生在写入操作之前。如果你没有加上1秒钟的等待,那么完全可能发生读取到的 done 为 false;而通过 channel 通信来实现共享内存就避免了这个问题,因为 done 的读取会等待写入完成。总之这里的关键就是尽量用 channel 实现 goroutine 之间的通信,避免使用共享内存;同时在并发编程时尽量使用 sync 包提供的各种同步锁和条件变量等等机制,避免使用类似“time.Sleep()”这种不可靠的方式去实现 goroutine 同步。我的理解对吗?
- GPT 说:
你的理解是完全正确的。在 Go 中,我们应该优先使用 channel 来在 goroutine 之间传递数据,而不是通过共享内存。这样可以避免数据竞争,使得代码更容易理解和维护。
同时,你也正确地指出了在并发编程中,我们应该使用
sync包提供的同步原语,如 Mutex、WaitGroup 和 Cond,来同步 goroutine 的执行。这些同步原语可以帮助我们建立明确的 happens-before 关系,确保我们的程序按照预期的顺序执行。最后,你也正确地指出了,我们应该避免使用
time.Sleep()这样的方式来同步 goroutine。因为time.Sleep()并不能保证 happens-before 关系,所以它不能确保 goroutine 的执行顺序。在实际的并发程序中,我们应该使用 channel、Mutex、WaitGroup 或者其他同步原语来同步 goroutine,而不是依赖于time.Sleep()。
2.3 Race Detector 检测数据竞争
- 我问 GPT:
最后,我还想知道 Race Detector 是什么
- GPT 说:
Go 语言提供了一个名为 Race Detector 的工具,用于检测代码中的数据竞争条件。数据竞争发生在当两个或更多的 goroutine 并发访问同一块内存区域,且至少有一个访问是写入操作时。
Race Detector 可以在运行时检测到这些数据竞争,帮助你找到并修复它们。你可以通过在
go run、go test或go build命令后添加-race选项来启用 Race Detector。以下是一个有数据竞争的程序:
package main import (
"fmt"
) var counter int func increment() {
counter++
} func main() {
go increment()
go increment()
fmt.Println(counter)
}
在这个程序中,两个 goroutine 都在尝试增加
counter的值。这会导致数据竞争,因为counter++不是一个原子操作。如果你使用 Race Detector 来运行这个程序(
go run -race main.go),它会报告数据竞争,并给出详细的报告,包括数据竞争发生的位置和涉及的 goroutine。注意,虽然 Race Detector 是一个非常有用的工具,但它可能会使你的程序运行得更慢,并使用更多的内存。因此,你应该只在测试和调试时启用它,而不是在生产环境中使用。
看起来挺有道理的,我试下这段代码:
$ go run -race main.go
0
==================
WARNING: DATA RACE
Write at 0x00010269a8a0 by goroutine 7:
main.increment()
/Users/danielhu/go/src/demo/main.go:10 +0x3c
Previous read at 0x00010269a8a0 by main goroutine:
main.main()
/Users/danielhu/go/src/demo/main.go:16 +0x44
Goroutine 7 (running) created at:
main.main()
/Users/danielhu/go/src/demo/main.go:15 +0x34
==================
Found 1 data race(s)
exit status 66
到这里,我还想追问一句:
- 我问 GPT:
所以 Race Detector 其实并不是为了检测并发访问一段数据,而是检测哪些并发访问数据时没有正确地使用合适的同步原语对吗?
- GPT 说:
是的,你的理解是正确的。Race Detector 的主要目的是检测数据竞争,也就是多个 goroutine 同时访问同一块数据,且至少有一个 goroutine 在进行写操作,而这些操作之间没有进行适当的同步。
如果你的代码中存在数据竞争,那么你的代码的行为就是未定义的,可能会导致各种难以预料和难以调试的问题。通过使用 Race Detector,你可以在开发和测试阶段就发现这些问题,然后通过添加适当的同步原语(如互斥锁、channel 等)来修复它们。
所以,Race Detector 并不是用来检测并发访问数据本身,而是用来检测并发访问数据时是否存在数据竞争,即是否存在没有进行适当同步的并发读写操作。
三、总结
三篇《跟着 GPT-4 从0到1学习 Golang 并发机制》文章到此写完。温故知新,收获颇丰,随便总结下:
- Goroutine:Goroutine 是 Go 语言的并发单元,属于轻量级的线程,由 Go runtime 管理,也可称协程。
- Channel:Channel 是 Go 语言中在 goroutines 之间通信的主要方式。
- Select:
select语句可以用于在多个 channel 操作之间进行选择。当多个 case 都可以运行时,select会随机选择一个。 - Mutexes 和 sync 包:
sync包提供了一些同步原语,如 Mutex(互斥锁)、RWMutex(读写锁)、WaitGroup(等待组)和 Cond(条件变量)。这些原语可以用于保护共享数据,或者同步 goroutines 的执行。 - 原子操作:Go 语言的 sync/atomic 包提供了一些原子操作函数,这些函数可以在多个 goroutine 之间安全地操作数据,而不需要使用互斥锁。原子操作包括 AddInt32、AddInt64、CompareAndSwapInt32、CompareAndSwapInt64 等等。这些函数可以保证在任何时刻只有一个 goroutine 能够对数据进行操作,从而避免数据竞争。
- Context 包:
context包提供了一种在 API 边界之间传递请求范围的值、取消信号和超时信息的方式。你可以使用context.WithCancel、context.WithDeadline、context.WithTimeout和context.WithValue函数来创建新的 Context。 - 内存模型:Go 的内存模型定义了在并发程序中,如何以及何时对变量的读写操作对其他 goroutine 可见。你应该使用 channel 或者
sync包中的原语来同步数据,以确保 happens-before 关系。 - Race Detector:Race Detector 是一个用于检测数据竞争的工具。你可以在
go run、go test或go build命令后添加-race选项来启用 Race Detector。 - Go 并发哲学:"Do not communicate by sharing memory; instead, share memory by communicating." 这是 Go 并发哲学的核心原则。它鼓励我们使用 channel 来在 goroutine 之间传递数据,而不是通过共享内存。这样可以避免数据竞争,使得代码更容易理解和维护。
Do not communicate by sharing memory; instead, share memory by communicating.
Do not communicate by sharing memory; instead, share memory by communicating.
Do not communicate by sharing memory; instead, share memory by communicating.
相关文章
(关注我的个人公众号“胡说云原生”吧)
跟着 GPT-4 从0到1学习 Golang 并发机制(三)的更多相关文章
- Python3.0科学计算学习之绘图(三)
matplotlib对象: 使用matplotlib的pyplot模块,可以供用户直接使用最重要的绘图命令.多数情况下,我们希望创建一个图形并且立即展示出来,但是有时如果生成要通过更改其属性来修改的图 ...
- 【java并发编程艺术学习】(三)第二章 java并发机制的底层实现原理 学习记录(一) volatile
章节介绍 这一章节主要学习java并发机制的底层实现原理.主要学习volatile.synchronized和原子操作的实现原理.Java中的大部分容器和框架都依赖于此. Java代码 ==经过编译= ...
- 新手入门指导:Vue 2.0 的建议学习顺序
起步 1. 扎实的 JavaScript / HTML / CSS 基本功.这是前置条件. 2. 通读官方教程 (guide) 的基础篇.不要用任何构建工具,就只用最简单的 <script> ...
- Retrofit2.0通俗易懂的学习姿势,Retrofit2.0 + OkHttp3 + Gson + RxJava
Retrofit2.0通俗易懂的学习姿势,Retrofit2.0 + OkHttp3 + Gson + RxJava Retrofit,因为其简单与出色的性能,也是受到很多人的青睐,但是他和以往的通信 ...
- 新手向:Vue 2.0 的建议学习顺序
新手向:Vue 2.0 的建议学习顺序 尤雨溪 1 年前 注:2.0 已经有中文文档 .如果对自己英文有信心,也可以直接阅读英文文档.此指南仅供参考,请根据自身实际情况灵活调整.欢迎转载,请注明出 ...
- 【数据库】7.0 MySQL入门学习(七)——MySQL基本指令:帮助、清除输入、查询等
1.0 help == ? 帮助指令,查询某个指令的解释.用法.说明等.详情参考博文: [数据库]6.0 MySQL入门学习(六)——MySQL启动与停止.官方手册.文档查询 https://www. ...
- 从0到1学习node之简易的网络爬虫
本文地址: http://www.xiabingbao.com/node/2017/01/19/node-spider.html 我们这节的目标是学习完本节课程后,能进行网页简单的分析与抓取,对抓取到 ...
- 【Spark2.0源码学习】-1.概述
Spark作为当前主流的分布式计算框架,其高效性.通用性.易用性使其得到广泛的关注,本系列博客不会介绍其原理.安装与使用相关知识,将会从源码角度进行深度分析,理解其背后的设计精髓,以便后续 ...
- [Android FrameWork 6.0源码学习] View的重绘过程之WindowManager的addView方法
博客首页:http://www.cnblogs.com/kezhuang/p/关于Activity的contentView的构建过程,我在我的博客中已经分析过了,不了解的可以去看一下<[Andr ...
- spark2.0源码学习
[Spark2.0源码学习]-1.概述 [Spark2.0源码学习]-2.一切从脚本说起 [Spark2.0源码学习]-3.Endpoint模型介绍 [Spark2.0源码学习]-4.Master启动 ...
随机推荐
- Python pip速度慢,更换源
版权声明:本文为CSDN博主「cocoprince」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明.原文链接:https://blog.csdn.net/Coco ...
- NC23054 华华开始学信息学
题目链接 题目 题目描述 因为上次在月月面前丢人了,所以华华决定开始学信息学.十分钟后,他就开始学树状数组了.这是一道树状数组的入门题: 给定一个长度为 \(N\) 的序列 \(A\) ,所有元素初值 ...
- php获取未解码之前的原始接口请求参数
前言 目前的几个项目,业务方基本都使用POST方式请求接口,我们本机磁盘会保留一份请求的原始参数用于请求分析和问题排查使用,一般有问题,也会基于seqid(请求唯一id)捞到日志,copy参数模拟请求 ...
- #Powerquery pq从入门到精通,笔记梳理
详细视频可以B站搜索,ms_excel博主,看leila的视频翻译课,powerquery从入门到精通. 后续也会根据自己工作来更新这部分的实际用法.
- 2020-09-08:KVM和OpenStack的区别?
福哥答案2020-09-08:[此答案来自知乎](https://www.zhihu.com/question/419987391)KVM只是一个虚拟机技术,别的还有xen,商业的vmware.vir ...
- 2021-05-26:给定一个char[][] matrix
2021-05-26:给定一个char[][] matrix,也就是char类型的二维数组,再给定一个字符串word,可以从任何一个某个位置出发,可以走上下左右,能不能找到word?char[][] ...
- 2021-07-07:股票问题4。给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成
2021-07-07:股票问题4.给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格.设计一个算法来计算你所能获取的最大利润.你最多可以完成 ...
- [AGC001E] BBQ Hard题解
Problem [AGC001E] BBQ Hard 计算: \[\sum_{i=1}^{n}\sum_{j=i+1}^n\binom{a_i+b_i+a_j+b_j}{a_i+a_j} \] 其中\ ...
- Strings must be encoded before hashing
Strings must be encoded before hashing 当我们将字符串传递给 hash 算法时,会出现 "TypeError: Strings must be enco ...
- Midjourney|文心一格prompt教程[Text Prompt(上篇)]:品牌log、App、徽章、插画、头像场景生成,各种风格选择:科技风、运动风
Midjourney|文心一格prompt教程[Text Prompt(上篇)]:品牌log.App.徽章.插画.头像场景生成,各种风格选择:科技风.运动风 1.撰写 Text Prompt 注意事项 ...