一、前言

话接上回《跟着 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)
}
}

在这个示例中,setup goroutine 对 adone 的写入操作可能对 main goroutine 不可见,即使我们在 setupmain 之间添加了 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 的访问。这样,我们就可以确保 setup goroutine 对 adone 的写入操作对 main goroutine 可见。

这个结果看起来并不好,我不满意。

  • 我接着问 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 rungo testgo 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 并发机制》文章到此写完。温故知新,收获颇丰,随便总结下:

  1. Goroutine:Goroutine 是 Go 语言的并发单元,属于轻量级的线程,由 Go runtime 管理,也可称协程。
  2. Channel:Channel 是 Go 语言中在 goroutines 之间通信的主要方式。
  3. Selectselect 语句可以用于在多个 channel 操作之间进行选择。当多个 case 都可以运行时,select 会随机选择一个。
  4. Mutexes 和 sync 包sync 包提供了一些同步原语,如 Mutex(互斥锁)、RWMutex(读写锁)、WaitGroup(等待组)和 Cond(条件变量)。这些原语可以用于保护共享数据,或者同步 goroutines 的执行。
  5. 原子操作:Go 语言的 sync/atomic 包提供了一些原子操作函数,这些函数可以在多个 goroutine 之间安全地操作数据,而不需要使用互斥锁。原子操作包括 AddInt32、AddInt64、CompareAndSwapInt32、CompareAndSwapInt64 等等。这些函数可以保证在任何时刻只有一个 goroutine 能够对数据进行操作,从而避免数据竞争。
  6. Context 包context 包提供了一种在 API 边界之间传递请求范围的值、取消信号和超时信息的方式。你可以使用 context.WithCancelcontext.WithDeadlinecontext.WithTimeoutcontext.WithValue 函数来创建新的 Context。
  7. 内存模型:Go 的内存模型定义了在并发程序中,如何以及何时对变量的读写操作对其他 goroutine 可见。你应该使用 channel 或者 sync 包中的原语来同步数据,以确保 happens-before 关系。
  8. Race Detector:Race Detector 是一个用于检测数据竞争的工具。你可以在 go rungo testgo build 命令后添加 -race 选项来启用 Race Detector。
  9. 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 并发机制(三)的更多相关文章

  1. Python3.0科学计算学习之绘图(三)

    matplotlib对象: 使用matplotlib的pyplot模块,可以供用户直接使用最重要的绘图命令.多数情况下,我们希望创建一个图形并且立即展示出来,但是有时如果生成要通过更改其属性来修改的图 ...

  2. 【java并发编程艺术学习】(三)第二章 java并发机制的底层实现原理 学习记录(一) volatile

    章节介绍 这一章节主要学习java并发机制的底层实现原理.主要学习volatile.synchronized和原子操作的实现原理.Java中的大部分容器和框架都依赖于此. Java代码 ==经过编译= ...

  3. 新手入门指导:Vue 2.0 的建议学习顺序

    起步 1. 扎实的 JavaScript / HTML / CSS 基本功.这是前置条件. 2. 通读官方教程 (guide) 的基础篇.不要用任何构建工具,就只用最简单的 <script> ...

  4. Retrofit2.0通俗易懂的学习姿势,Retrofit2.0 + OkHttp3 + Gson + RxJava

    Retrofit2.0通俗易懂的学习姿势,Retrofit2.0 + OkHttp3 + Gson + RxJava Retrofit,因为其简单与出色的性能,也是受到很多人的青睐,但是他和以往的通信 ...

  5. 新手向:Vue 2.0 的建议学习顺序

    新手向:Vue 2.0 的建议学习顺序 尤雨溪   1 年前 注:2.0 已经有中文文档 .如果对自己英文有信心,也可以直接阅读英文文档.此指南仅供参考,请根据自身实际情况灵活调整.欢迎转载,请注明出 ...

  6. 【数据库】7.0 MySQL入门学习(七)——MySQL基本指令:帮助、清除输入、查询等

    1.0 help == ? 帮助指令,查询某个指令的解释.用法.说明等.详情参考博文: [数据库]6.0 MySQL入门学习(六)——MySQL启动与停止.官方手册.文档查询 https://www. ...

  7. 从0到1学习node之简易的网络爬虫

    本文地址: http://www.xiabingbao.com/node/2017/01/19/node-spider.html 我们这节的目标是学习完本节课程后,能进行网页简单的分析与抓取,对抓取到 ...

  8. 【Spark2.0源码学习】-1.概述

          Spark作为当前主流的分布式计算框架,其高效性.通用性.易用性使其得到广泛的关注,本系列博客不会介绍其原理.安装与使用相关知识,将会从源码角度进行深度分析,理解其背后的设计精髓,以便后续 ...

  9. [Android FrameWork 6.0源码学习] View的重绘过程之WindowManager的addView方法

    博客首页:http://www.cnblogs.com/kezhuang/p/关于Activity的contentView的构建过程,我在我的博客中已经分析过了,不了解的可以去看一下<[Andr ...

  10. spark2.0源码学习

    [Spark2.0源码学习]-1.概述 [Spark2.0源码学习]-2.一切从脚本说起 [Spark2.0源码学习]-3.Endpoint模型介绍 [Spark2.0源码学习]-4.Master启动 ...

随机推荐

  1. Terraform 系列-Terraform 简介

    系列文章 Terraform 系列文章 前言 最近在使用 Terraform 来置备 OCI 的 Always Free Tier, 发现它非常好用.总结学习下:Terraform 的基础知识. 什么 ...

  2. JSON.parse 函数 (JavaScript)

    将 JavaScript 对象表示法 (JSON) 字符串转换为对象. 语法 参数 返回值 异常 以下示例使用 JSON.parse 将 JSON 字符串转换成对象. var jsontext = ' ...

  3. Java中「Future」接口详解

    目录 一.背景 二.Future接口 1.入门案例 2.Future接口 三.CompletableFuture类 1.基础说明 2.核心方法 2.1 实例方法 2.2 计算方法 2.3 结果获取方法 ...

  4. javasec(五)URLDNS反序列化分析

    这篇文章介绍 URLDNS 就是ysoserial中⼀个利⽤链的名字,但准确来说,这个其实不能称作"利⽤链".因为其参数不是⼀个可以"利⽤"的命令,⽽仅为⼀个U ...

  5. 2.JAVA入门基础知识

    数据类型: java的数据类型分为两大类:基本类型和引用类型 基本类型: 整数类型: byte 一个字节 -128-127 short 2个字节 32768-32767 int 4个字节 很大 lon ...

  6. 笔记:C++学习之旅---面向对象程序设计2

    笔记:C++学习之旅---面向对象程序设计2 面向对象程序设计基于三个基本概念:数据抽象.继承和动态绑定. 继承和动态绑定对程序的编写有两方面的影响:一是我们可以更容易的定义与其他类相似但不完全相同的 ...

  7. VS2022使用ClickOnce发布程序本地安装.net框架

    因为遇到下面的错误,没有在网上搜到详细解决问题的教程,费了一些时间才解决了问题,特此记录一下,也希望能帮助到其他人. 要在"系统必备"对话框中启用"从与我的应用程序相同的 ...

  8. 【Azure 存储服务】使用 AppendBlobClient 对象实现对Blob进行追加内容操作

    问题描述 在Azure Blob的官方示例中,都是对文件进行上传到Blob操作,没有实现对已创建的Blob进行追加的操作.如果想要实现对一个文件的多次追加操作,每一次写入的时候,只传入新的内容? 问题 ...

  9. 获取scrollTop的方法(兼容所有浏览器)

    /** *获取scrollTop的值,兼容所有浏览器 */ function getScrollTop() { var scrollTop = document.documentElement.scr ...

  10. [MAUI程序设计]界面多态与实现

    目录 需求一:在不同设备上使用不同 UI 外观 定义控件 UI 外观 基于平台自定义配置 需求二:在不同数据类别中使用不同的 UI 外观 定义视图 UI 外观 创建数据模板 创建选择器 定义数据 需求 ...