Go并发编程-极客时间专栏学习总结

发布于 2020-10-12 · 本文总共 5327 字 · 阅读大约需要 16 分钟

前言

任务编排用Channel,共享资源保护用传统并发原语

“针对同一种场景,也许存在很多并发原语都适用的情况,但是一定是有最合适的那一个。 所以,你必须非常清楚每种并发原语的实现机制和适用场景,千万不要被网上的一些文章误导,万事皆用 Channel。”

summary

  • 并发编程,为什么选Go?

“一句话,我被 Go 的简单高效所打动。它不仅部署方便,自带完善的工具链, 特别是 Go 在处理并发场景上表现出的独特性能,更是让我着迷。”

“Go 并发编程的重要性不容置疑。只要是使用 Go 开发的大型应用程序,并发是必然要采用的技术。”

  • 怎么提升 Go 并发编程能力?

知识主线:

基本并发原语–>原子操作–>Channel–>扩展并发原语–>分布式并发原语

学习主线:

基本用法–>实现原理–>易错场景–>知名项目中的Bug

课程模块

基本并发原语:

介绍 Mutex、RWMutex、Waitgroup、Cond、Pool、Context 等标准库中的并发原语, 这些都是传统的并发原语,在其它语言中也很常见,是我们在并发编程中常用的类型;

原子操作:

介绍Go 标准库中提供的原子操作。原子操作是其它并发原语的基础,学会了就可以自己创造新的并发原语;

Channel:

Channel 类型是 Go 语言独特的类型,因为比较新,所以难以掌握。 基本用法,处理场景和应用模式,避免踩坑;

扩展并发原语:

目前来看,Go 开发组不准备在标准库中扩充并发原语了,但是还有一些并发原语应用广泛, 比如信号量、SingleFlight、循环栅栏、ErrGroup 等。 掌握了它们,就可以在处理一些并发问题时,取得事半功倍的效果;

分布式并发原语:

分布式并发原语是应对大规模的应用程序中并发问题的并发类型。 介绍使用 etcd 实现的一些分布式并发原语,比如 Leader 选举、分布式互斥锁、分布式读写锁、分布式队列等, 在处理分布式场景的并发问题时,特别有用;

refs

https://time.geekbang.org/column/article/294849

https://myslide.cn/slides/23014#

2020.10.13更新

基本并发源语

1.Mutex:如何解决资源并发访问问题

go并发

 import (
        "fmt"
        "sync"
    )
    
    func main() {
        var count = 0
        // 使用WaitGroup等待10个goroutine完成
        var wg sync.WaitGroup
        wg.Add(10)
        for i := 0; i < 10; i++ {
            go func() {
                defer wg.Done()
                // 对变量count执行10次加1
                for j := 0; j < 100000; j++ {
                    count++
                }
            }()
        }
        // 等待10个goroutine完成
        wg.Wait()
        fmt.Println(count)
    }
 	// 	310909
	// 224339
	// 198934   

count++ 不是一个原子操作,它至少包含几个步骤,比如读取变量 count 的当前值,对这个值加 1,把结果再保存到 count 中。因为不是原子操作,就可能有并发的问题。

 // count++操作的汇编代码
    MOVQ    "".count(SB), AX
    LEAQ    1(AX), CX
    MOVQ    CX, "".count(SB)

Go race detector

Go 提供了一个检测并发访问共享资源是否有问题的工具: race detector,它可以帮助我们自动发现程序有没有 data race 的问题。

Go race detector 是基于 Google 的 C/C++ sanitizers 技术实现的,编译器通过探测所有的内存访问,加入代码能监视对这些内存地址的访问(读还是写)。在代码运行的时候,race detector 就能监控到对共享变量的非同步访问,出现 race 的时候,就会打印出警告信息。

Go 1.1 中就引入了这种技术,并且一下子就发现了标准库中的 42 个并发问题。现在,race detector 已经成了 Go 持续集成过程中的一部分。

go run -race count.go

qwq go_practice % go run -race count.go
==================
WARNING: DATA RACE
Read at 0x00c0000ce008 by goroutine 8:
  go_practice/go_concurrency.Count.func1()
      /root/GO/src/go_practice/go_concurrency/1.go:22 +0x75

Previous write at 0x00c0000ce008 by goroutine 7:
  go_practice/go_concurrency.Count.func1()
      /root/GO/src/go_practice/go_concurrency/1.go:22 +0x8b

Goroutine 8 (running) created at:
  go_practice/go_concurrency.Count()
      /root/GO/src/go_practice/go_concurrency/1.go:18 +0xe4
  main.main()
      /root/GO/src/go_practice/count.go:6 +0x2f

Goroutine 7 (running) created at:
  go_practice/go_concurrency.Count()
      /root/GO/src/go_practice/go_concurrency/1.go:18 +0xe4
  main.main()
      /root/GO/src/go_practice/count.go:6 +0x2f
==================
216910
Found 1 data race(s)
exit status 66

Go race detector只能通过真正对实际地址进行读写访问的时候才能探测, 所以它并不能在编译的时候发现 data race 的问题

Mutex

package main


    import (
        "fmt"
        "sync"
    )


    func main() {
        // 互斥锁保护计数器
        var mu sync.Mutex
        // 计数器的值
        var count = 0
        
        // 辅助变量,用来确认所有的goroutine都完成
        var wg sync.WaitGroup
        wg.Add(10)

        // 启动10个gourontine
        for i := 0; i < 10; i++ {
            go func() {
                defer wg.Done()
                // 累加10万次
                for j := 0; j < 100000; j++ {
                    mu.Lock()
                    count++
                    mu.Unlock()
                }
            }()
        }
        wg.Wait()
        fmt.Println(count)
    }

使用

很多情况下,Mutex 会嵌入到其它 struct 中使用;

可以采用嵌入字段的方式。通过嵌入字段,你可以在这个 struct 上直接调用 Lock/Unlock 方法。

func main() {
    var counter Counter
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 100000; j++ {
                counter.Lock()
                counter.Count++
                counter.Unlock()
            }
        }()
    }
    wg.Wait()
    fmt.Println(counter.Count)
}


type Counter struct {
    sync.Mutex
    Count uint64
}

还可以把获取锁、释放锁、计数加一的逻辑封装成一个方法,对外不需要暴露锁等逻辑

func main() {
    // 封装好的计数器
    var counter Counter

    var wg sync.WaitGroup
    wg.Add(10)

    // 启动10个goroutine
    for i := 0; i < 10; i++ {
        go func() {
            defer wg.Done()
            // 执行10万次累加
            for j := 0; j < 100000; j++ {
                counter.Incr() // 受到锁保护的方法
            }
        }()
    }
    wg.Wait()
    fmt.Println(counter.Count())
}

// 线程安全的计数器类型
type Counter struct {
    CounterType int
    Name        string

    mu    sync.Mutex
    count uint64
}

// 加1的方法,内部使用互斥锁保护
func (c *Counter) Incr() {
    c.mu.Lock()
    c.count++
    c.mu.Unlock()
}

// 得到计数器的值,也需要锁保护
func (c *Counter) Count() uint64 {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

a static race detector

https://github.com/amit-davidson/Chronos

go get -v github.com/amit-davidson/Chronos/cmd/chronos

chronos --file <path_to_main>


help

Usage of ./chronos:
  -file string
    	The file containing the entry point of the program
  -pkg string
    	Path to the to pkg of the file. Tells Chronos where to perform the search. By default, it assumes the file is inside $GOPATH    

Support:

Detects races on pointers passed around the program.
Analysis of conditional branches, nested functions, interfaces, select, gotos, defers, for loops and recursions.
Synchronization using mutex and goroutines starts.

Limitations:

Big programs and external packages. (Due to stack overflow)
Synchronization using channels, waitgroups, once, cond and atomic.

refs

https://github.com/google/sanitizers

https://blog.golang.org/race-detector

https://github.com/moby/moby/pull/37583

https://github.com/moby/moby/pull/35517

https://github.com/moby/moby/pull/32826

https://github.com/moby/moby/pull/30696

https://github.com/kubernetes/kubernetes/pull/72361

https://github.com/kubernetes/kubernetes/pull/71617

https://colobu.com/2018/12/18/dive-into-sync-mutex/

https://studygolang.com/articles/17017

https://golang.org/src/sync/mutex.go

2020.11.10更新

2.Mutex:庖丁解牛看实现

3.Mutex:4种易错场景

4.Mutex:如何扩展额外功能

5.RWMutex:读写锁的实现原理及避坑指南

6.WaitGroup:协同等待,任务编排利器

7.Cond:条件变量的实现机制及避坑指南

8.Once:一个简约而不简单的同步原语

9.Map:如何实现线程安全的Map类型

10.Pool:性能提升大杀器

11.Context:信息穿透上下文

原子操作

12.atomic:要保证原子操作,一定要使用这几种方法

Channel

13.Channel:另辟蹊径,解决并发问题

14.Channel:透过代码看典型的应用模式

15.内存模型:Go如何保证并发读写的顺序?

扩展并发原语

16.Samaphore:一篇文章搞懂信号量

17.SingleFlight和CyclicBarrier:请求合并和循环栅栏

18.分组操作:处理一组子任务,该用什么并发原语

分布式并发原语

19.在分布式环境中,Leader选举和互斥锁该如何实现?

20.在分布式环境中,队列、STM该如何实现?

结束语

refs

2020 新春流行的RPC框架性能大比拼: https://colobu.com/2020/01/21/benchmark-2019-spring-of-popular-rpc-frameworks/

鸟窝: https://colobu.com/

https://www.youtube.com/watch?v=f6kdp27TYZs

https://github.com/golang/go/issues/5045




本博客所有文章采用的授权方式为 自由转载-非商用-非衍生-保持署名 ,转载请务必注明出处,谢谢。
声明:
本博客欢迎转发,但请保留原作者信息!
博客地址:邱文奇(qiuwenqi)的博客;
内容系本人学习、研究和总结,如有雷同,实属荣幸!
阅读次数:

文章评论

comments powered by Disqus


章节列表