前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Go语言基础6 - 并发

Go语言基础6 - 并发

作者头像
张云飞Vir
发布于 2020-03-16 08:55:13
发布于 2020-03-16 08:55:13
48300
代码可运行
举报
文章被收录于专栏:写代码和思考写代码和思考
运行总次数:0
代码可运行

概述

我们将用几节来学习Go语言基础,本文结构如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1. 并发
  通过通信共享内存
  Go程
  信道
  信道中的信道
  并行化
  可能泄露的缓冲区
2. 错误
  Panic
  恢复

1. 并发

1.1 通过通信共享内存

在并发编程中,为实现对共享变量的正确访问需要精确的控制,这在多数环境下都很困难。

实际上,多个独立执行的线程从不会主动共享。Go语言另辟蹊径,它将共享的值通过信道传递, 在任意给定的时间点,只有一个Go程能够访问该值,数据竞争从设计上就被杜绝了。

例如,引用计数通过为整数变量添加互斥锁来很好地实现。 取而代之的是,通过信道来控制访问能够让你写出更简洁的程序。

Go将它简化为一句口号:

不要通过共享内存来通信,而应通过通信来共享内存。

1.2 Go程

Go程具有简单的模型:

  • 它是与其它Go程并发运行在同一地址空间的函数。
  • 它是轻量级的, 消耗几乎就只有栈空间的分配。
  • 而且栈最开始是非常小的,所以它们很廉价, 仅在需要时才会随着堆空间的分配(和释放)而变化。

Go程在多线程操作系统上可实现多路复用,因此若一个线程阻塞,比如说等待I/O, 那么其它的线程就会运行。 Go程的设计隐藏了线程创建和管理的诸多复杂性。

在函数或方法前添加 go 关键字能够在新的Go程中调用它。当调用完成后, 该Go程也会安静地退出,示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
go list.Sort()  // 并发运行 list.Sort,无需等它结束。

函数字面在Go程调用中非常有用。 备注:可理解 为匿名函数的调用。下面的方法先声明了一个匿名方法,然后立即调用。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func Announce(message string, delay time.Duration) {
      go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  // 注意括号 - 必须调用该函数。
}

在Go中,函数字面都是闭包:其实现在保证了函数内引用变量的生命周期与函数的活动时间相同。

1.3 信道( chan )

1.3.1 格式: make(chan int)

信道与映射一样,也需要通过 make 来分配内存,make 后的返回值是对底层数据结构的引用。 若提供了一个可选的整数形参,它就会为该信道设置缓冲区大小。 缓冲区大小的默认值是零,表示不带缓冲的或同步的信道。

示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ci := make(chan int)            // 整数类型的无缓冲信道
cj := make(chan int, 0)         // 整数类型的无缓冲信道
cs := make(chan *os.File, 100)  // 指向文件指针的带缓冲信道

无缓冲信道在通信时会同步交换数据,它能确保(两个Go程的)计算处于确定状态。

1.3.2 阻塞等待Go程( 无缓冲区的示例 )

示例:使用 go 程,在后台启动了排序操作,等待排序完成。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
c := make(chan int)  // 分配一个信道
// 在Go程中启动排序。当它完成后,在信道上发送信号。
go func() {
    list.Sort()
    c <- 1  // 发送信号,什么值无所谓。
}()
doSomethingForAWhile()
<-c   // 等待排序结束,丢弃发来的值。
  • 接收者在收到数据前会一直阻塞。
  • 若信道是不带缓冲的,那么在接收者收到值前, 发送者会一直阻塞;
  • 若信道是带缓冲的,则发送者仅在值被复制到缓冲区前阻塞;
  • 若缓冲区已满,发送者会一直等待直到某个接收者取出一个值为止。
1.3.3 控制吞吐量的例子( 带缓冲的示例 )

带缓冲的信道可被用作信号量。例如限制吞吐量。

示例: var sem = make(chan int, MaxOutstanding)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  func handle(r *Request) {               
    sem <- 1 // 等待活动队列清空。            #2  占据,阻塞
    process(r)  // 可能需要很长时间。
    <-sem    // 完成;使下一个请求可以运行。    #3  解除占据
  }

  func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)  // 无需等待 handle 结束。 #1 每个请求对应一个 Go程
    }
  }

上面的例子中:

  • 进入的请求 req 会被传递给 handle。
  • handle 中 #2 等待一个信号继续(当缓冲区满时)
  • handle 中 #3 后,发送信号,使得 被阻塞的另一个 go程 开始进入到process
  • 信道缓冲区的容量决定了同时调用 process 的数量上限

备注: 这个示例一次开始了全部多个go程,然后根据缓冲区大小阻塞等待,当缓冲区可以进入时继续进行。

1.3.4 继续改良的例子( 采用匿名方法 )

若请求来得很快, 上面的程序就会无限地消耗资源。为了弥补这种不足,我们可以通过修改 Serve 来限制创建Go程:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func() {
            process(req) // 这儿有Bug,解释见下。
            <-sem
        }()
    }
  }

Bug出现在Go的 for 循环中,该循环变量在每次迭代时会被重用,因此 req 变量会在所有的Go程间共享,这不是我们想要的。我们需要确保 req 对于每个Go程来说都是唯一的。 有一种方法能够做到,就是将 req 的值作为实参传入到该Go程的闭包中:

func Serve(queue chan *Request) { for req := range queue { sem <- 1 go func(req *Request) { process(req) <-sem }(req) } }

闭包的处理 比较前后两个版本,观察该闭包声明和运行中的差别。 另一种解决方案就是以相同的名字创建新的变量,如例中所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func Serve(queue chan *Request) {
    for req := range queue {
        req := req // 为该Go程创建 req 的新实例。
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

它的写法看起来有点奇怪

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
req := req

但在Go中这样做是合法且惯用的。你用相同的名字获得了该变量的一个新的版本, 以此来局部地刻意屏蔽循环变量,使它对每个Go程保持唯一。

1.3.5 固定数据的go程,同时读取

另一种管理资源的好方法:

  • 启动固定数量的 handle Go程,一起从请求信道中读取数据。
  • Go程的数量限制了同时调用 process 的数量。
  • Serve 同样会接收一个通知退出的信道, 在启动所有Go程后,它将阻塞并暂停从信道中接收消息。 func handle(queue chan *Request) { for r := range queue { process(r) } } func Serve(clientRequests chan *Request, quit chan bool) { // 启动处理程序,固定数量 for i := 0; i < MaxOutstanding; i++ { go handle(clientRequests) } <-quit // 等待通知退出。 }

1.4 信道中的信道

这种特性通常被用来实现安全、并行的多路分解。

在上一节的例子中,handle 是个非常理想化的请求处理程序, 但我们并未定义它所处理的请求类型。若该类型包含一个可用于回复的信道, 那么每一个客户端都能为其回应提供自己的路径。以下为 Request 类型的大概定义。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}

客户端提供了一个函数及其实参,此外在请求对象中还有个接收应答的信道。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func sum(a []int) (s int) {
    for _, v := range a {
        s += v
    }
    return
}

request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// 发送请求
clientRequests <- request
// 等待回应
fmt.Printf("answer: %d\n", <-request.resultChan)

服务端的处理 On the server side, the handler function is the only thing that changes.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func handle(queue chan *Request) {
    for req := range queue {
        req.resultChan <- req.f(req.args)
    }
}

1.5 并行化

这些设计的另一个应用是在多CPU核心上实现并行计算。如果计算过程能够被分为几块 可独立执行的过程,它就可以在每块计算结束时向信道发送信号,从而实现并行处理。

1.6 可能泄露的缓冲区

--

2. 错误

Go语言具有多值返回特性, 使得它可以在返回常规的值,和详细的错误描述。

按照约定,错误的类型通常为 error,这是一个内建的简单接口。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type error interface {
    Error() string
}

库的编写者通过更丰富的底层模型可以轻松实现这个接口,这样不仅能看见错误, 还能提供一些上下文。

例如,os.Open 可返回一个 os.PathError。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/* 定义结构体 */
// PathError 记录一个错误以及产生该错误的路径和操作。
type PathError struct {
    Op string    // "open"、"unlink" 等等。
    Path string  // 相关联的文件。
    Err error    // 由系统调用返回。
}

/* 实现 Error接口 */
func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

这样,PathError的 Error 会生成如下错误信息:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
open /etc/passwx: no such file or directory

错误字符串应尽可能地指明它们的来源,解释清楚错误的情况。

若调用者想知道更多细节,可使用类型选择或者类型断言来查看特定错误,和处理。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for try := 0; try < 2; try++ {
    file, err = os.Create(filename)
    if err == nil {
        return
    }
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles()  // 恢复一些空间。
        continue
    }
    return
}

上面的第5行,即第2条 if 是另一种类型断言。 若它失败, ok 将为 false,而 e 则为nil. 若它成功, ok 将为 true

2.1 Panic

有时程序就是不能继续运行。为此,可以使用内建的 panic 函数,它会产生一个运行时错误并终止程序。

该函数接受一个任意类型的实参(一般为字符串),并在程序终止时打印输出。格式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Panic( 字符串 )

实际使用中,库函数应避免 panic。若问题可以被屏蔽或解决, 最好就是让程序继续运行,而不是终止。

一个反例的情况就是初始化中: 若某个库真的不能让自己工作,那就触发Panic 吧,比如

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var user = os.Getenv("USER")

    func init() {
      if user == "" {
      panic("no value for $USER")
    }
}

2.2 恢复

当 panic 被调用后, 程序将立刻终止当前函数的执行,并开始回溯Go程的栈,运行任何被推迟的函数。 若回溯到达Go程栈的顶端,程序就会终止。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 //  我自己画的不太严谨的图例,帮助理解。
 //  假如在 main 函数里调用了 方法1,在 方法1 里又调用了 方法2
  |                              |
  |                              |
  | #4           方法2            |        // 假如在这里触发了 Panic
  | #3           方法2的defer     |    //在 defer 时,仍然有机会调用 recover函数来恢复
  | #2      方法1                |
  | #1   main                    |    //到这里就程序终止了
  -------------------------------

不过我们可以用内建的 recover 函数来 取回Go程的控制权限 并使其恢复正常执行。

调用 recover函数 将停止回溯过程,它的返回值是错误信息(实际是调用 panic 函数时的参数)。

由于在回溯时,只有被推迟的函数( defer )在运行,因此 recover 只能在被推迟(defer)的函数中才有效。

在 Go程 内通过 recover 来终止失败的Go程,而无需让整个程序崩溃。

先看示例代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}

在此例中,若 do(work) 触发了Panic,其结果就会被记录(打印输出), 而该Go程会被干净利落地结束,不会干扰到其它Go程。我们无需在推迟的闭包中做任何事情, recover 会处理好这一切。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
京喜小程序的高性能打造之路
京喜小程序自去年双十一上线微信购物一级入口后,时刻迎接着亿级用户量的挑战,细微的体验细节都有可能被无限放大,为此,“极致的页面性能”、“友好的产品体验” 和 “稳定的系统服务” 成为了我们开发团队的最基本执行原则。
用户6835371
2021/06/01
7790
京喜小程序的高性能打造之路
存量用户运营企业微信的“用户端小程序”优化方案
企业微信端产品“C端用户小程序”,是一款服务于vivo线下代理、门店和导购,帮助导购连接用户,快速与用户进行沟通的工具。基于“C端小程序”的PU/UV量较为庞大,为了更加极致的用户体验,所以提升小程序性能优化是必然。
2020labs小助手
2021/03/16
8610
京东京喜小程序的高性能打造之路
京喜小程序自去年双十一上线微信购物一级入口后,时刻迎接着亿级用户量的挑战,细微的体验细节都有可能被无限放大,为此,“极致的页面性能”、“友好的产品体验” 和 “稳定的系统服务” 成为了我们开发团队的最基本执行原则。
winty
2020/04/01
2.6K0
微信小程序-零基础入门手册
wx.showLoading(Object object) | 微信开放文档 (qq.com)
打不着的大喇叭
2024/03/11
3190
微信小程序-零基础入门手册
# 小程序性能优化和异常监控
小程序的渲染层和逻辑层分别由两个线程管理,两个线程的通信由微信客户端(Native)做中转。
九旬
2023/10/17
3520
小程序的异步加载与懒加载
随着小程序的不断发展,性能优化已成为提升用户体验的重要方面。异步加载和懒加载是两种常用的性能优化技术,通过它们可以有效减少页面加载的初始时间,提高用户体验。本文将详细介绍小程序中的异步加载与懒加载技术,探讨它们的工作原理及在实际项目中的应用,并提供相关的优化实践和代码示例。
LucianaiB
2025/01/28
1760
微信小程序性能优化总结
微信小程序提供了一个“体验评分”的工具插件,可以使用它获得微信小程序的一些性能数据和明显的缺陷,进而根据报告进行相应的优化。
xiangzhihong
2022/07/30
2.4K0
微信小程序性能优化总结
小程序之图片懒加载
懒加载,前端人都知道的一种性能优化方式,简单的来说,只有当图片出现在浏览器的可视区域内时,才设置图片正真的路径,让图片显示出来。这就是图片懒加载。
前端黑板报
2018/12/24
1.1K0
惊爆!掌握这些小程序性能优化技巧,让你的小程序速度飙升 10 倍
嘿,各位程序猿小伙伴们!是不是经常被自家小程序的 “蜗牛速度” 搞得焦头烂额?用户在那边疯狂吐槽,咱在这边急得抓耳挠腮。别担心,今天小编就来给大家放大招啦,只要掌握了这些小程序性能优化技巧,让你的小程序速度飙升 10 倍都不在话下!接下来,就跟着小编一起进入神奇的性能优化世界吧!
小白的大数据之旅
2025/04/27
1160
惊爆!掌握这些小程序性能优化技巧,让你的小程序速度飙升 10 倍
《腾讯大家》小程序开发总结
《腾讯大家》是公司推出的中文互联网专栏写作服务产品。由于寻找有效信息的成本是非常大的,一些真正具有传播价值的内容,却往往淹没于信息洪流之中。如何将最有价值的信息以最快的速度呈现给用户,正是《大家》产品设计的初衷。《大家》更关注互联网用户更深入、更持久的思考与表达。我们希望呈现给用户的,是经得起时间考验的文章,是时代最前沿的思想。它的表现,可能是一个专栏、一部电子书、一个属于个人的频道,甚至是一款小程序。
JoanLiu
2018/11/15
2.4K1
微信小程序优化uni-app
onLaunch 当uni-app初始化完成时触发 onShow 当uni-app启动,或从后台进入前台显示 onHide 当uni-app从前台进入后台
达达前端
2019/09/19
2.7K0
微信小程序优化uni-app
赶紧收藏!小程序开发中鲜为人知的代码优化技巧,效率提升必备
嘿,各位小程序开发的小伙伴们!是不是在开发小程序的时候,总感觉自己的代码就像那老旧的自行车,吭哧吭哧跑得慢,还老是出问题?别愁啦,今天小编就带着一堆超实用的小程序开发代码优化技巧来拯救你们啦!这些可都是鲜为人知的小秘密哦,掌握了它们,你的开发效率直接起飞,代码也会变得超级丝滑。准备好你的小本本,咱们马上发车!
小白的大数据之旅
2025/03/21
1390
我做了一个成语接龙的小程序
我是一名安卓程序员,以前没有接触过前端开发,直到有幸接手了公司的小程序项目。小程序学起来还是很快的,对于有编程经验的人,看着示例代码,对照着官方文档,几天就能上手了。
NanBox
2019/07/09
1.7K0
2019-面向小白的微信小程序-视频教学-基础
微信小程序,简称小程序,英文名Mini Program,是一种不需要下载安装即可使用的应用,它实现了应用“触手可及”的梦想,用户扫一扫或搜一下即可打开应用
万少
2025/02/08
2160
2019-面向小白的微信小程序-视频教学-基础
小程序的图片加载与优化
在小程序的开发过程中,图片作为一种重要的媒体资源,常常会在页面中占据重要位置。无论是应用的图标、背景图,还是商品展示图、广告图,图片的加载效果直接影响到用户体验。然而,图片加载也可能是小程序性能的瓶颈之一,尤其是在图片较多或者网络环境不稳定的情况下。因此,合理优化图片加载,不仅能够减少页面加载时间,还能有效提升小程序的响应速度。
LucianaiB
2025/01/28
2490
【小程序探索】:深入理解小程序中的数据
刚开始撸小程序的时候,觉得看看文档就可以了,导致写了很多垃圾代码坑人坑己,相信大部分初学者也不会去仔细研究文档,更别说啰里啰嗦的指南了,在通读小程序官方指南后,很有必要总结一番。清楚了生命周期和数据通信,就能对整个程序有一定的把控能力,定位问题和解决问题的能力将大幅提高。
极乐君
2019/12/16
1.3K0
【小程序探索】:深入理解小程序中的数据
wx小程序--基础知识
微信小程序是腾讯于2017年1月9日推出的一种不需要下载安装即可在微信平台上使用的应用,主要提供给企业、政府、媒体、其他组织或个人的开发者在微信平台上提供服务。
eadela
2020/01/15
1.8K0
wx小程序--基础知识
小程序模板语法样式与页面配置
在 data 中定义数据,在 WXML 中使用数据。使用Mustache{{}}语法:
timerring
2023/06/09
6980
小程序模板语法样式与页面配置
# 小程序的优化
参考:在微信小程序中使用 less(最优方式)open in new window
九旬
2023/10/17
1880
微信小程序电影模板
Vant Weapp UI库使用Vant Weapp 地址: https://youzan.github.io/vant-weapp
达达前端
2019/08/08
4.7K0
微信小程序电影模板
推荐阅读
相关推荐
京喜小程序的高性能打造之路
更多 >
LV.0
这个人很懒,什么都没有留下~
目录
  • 概述
    • 1. 并发
      • 1.1 通过通信共享内存
      • 1.2 Go程
      • 1.3 信道( chan )
      • 1.4 信道中的信道
      • 1.5 并行化
      • 1.6 可能泄露的缓冲区
    • 2. 错误
      • 2.1 Panic
      • 2.2 恢复
      • 在 Go程 内通过 recover 来终止失败的Go程,而无需让整个程序崩溃。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档