Go语言并发编程与依赖管理 | 青训营笔记

112 阅读4分钟

Go语言并发编程与依赖管理 | 青训营笔记

参加 [第六届青训营] 笔记创作第四篇

本堂课的内容

分为3个部分:

  • 第一个部分介绍了Go语言高性能的本质
  • 第二个部分介绍了Go语言依赖管理的演化
  • 第三个部分是使用了一些测试demo让大家初初了解测试

Go的高性能本质

高效的并发模型

并发是指:多线程程序在一个核的cpu上运行,本质是多个线程对一个cpu的轮询使用。因为每个任务执行切换的速度比较快,让使用者认为是多个任务在同时执行,其实本质上只是单核cpu不断地切换任务来完成并发操作而已。

并行:多任务在同一个时刻同时执行,需要完成这一点则要求计算机需要有多核心多cpu,每个核心独立执行一个任务,多个任务同时执行,不需要进行任务切换。

可以明确的是并行的效率是高于并发的,因为这里少了切换任务的cpu开销。

Go语言因为其调度模型的完善可以充分发挥多核的性能,在程序任务执行的时候可以高效运行,所以说Go语言是一门高性能语言,很适合处理并发任务。

协程

协程和线程的区别

  1. 线程
    • 昂贵的系统资源:栈为MB级别的。
    • 工作在内核态。
    • 创建、切换、停止都是多cpu而言负担比较重的工作,是比较吃资源的。
  2. 协程
    • 可以看作是轻量级的线程:栈为KB级别的,一次可以创建上万个协程
    • 工作在用户态,少了用户态切换到内核态再从内核态切换到用户态等一系列操作。
    • 创建、切换、停止都是由Go语言完成的。

协程的创建:

创建调用比较简单,也就是函数前面加上关键字 go 即可,如:

// 快速打印 hello goroutine : 0~hello goroutine : 4
func hello(i int) {
    println("hello goroutine : " +fmt.Sprint(i))
}

func HelloGoRoutine() {
    for i := 0; i < 5; i++ {
        go func(j int) {
            hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}

channel通信机制

Go语言提倡通过通信来共享内存以达到协程间运行的信息能够准确无误,绝非是通过共享内存进而达到通信的效果。

channel 其实理解起来就像是所有协程共享的一个队列,里面存放的消息先进先出,协程能往 channel 里存数据,也能从 channel 中取数据,可以让一个 gorountine 发送特定的值到另一个 goroutine。

channel的创建:

也是使用make关键字来创建,比较类似于slice等结构的创建。根据是否有缓冲区,可以分为无缓冲通道、有缓冲通道。

make(chan 元素类型,[缓冲大小])

// 创建一个无缓冲区的 channel
src := make(chan int)
// 创建一个缓冲为3个字节的 channel
dest := make(chan int, 3)

一个通过channel 来共享内存的实例:

func CalSquare() {
    src := make(chan int)
    dest := make(chan int, 3)
    // 生产者
    go func() {
        defer close(src)
        for i := 0; i < 10; i++ {
            src <- i
        }
    }()
    // 消费者
    go func() {
        defer close(dest)
        for i := range src {
            dest <- i * i
        }
    }()
    // 生产者往 src put 的逻辑简单,而 println 打印消费流逻辑复杂。
    // 所以消费者消费速度慢,给 dest 设置缓冲区,避免生产者等着消费者消费而阻塞。
    for i := range dest {
        // 复杂操作
        println(i)
    }
}

临界区的保护

也是使用mutex来保护临界区,具体有一个 sync.Mutex api供使用者调用。

实例:

var (
    x int64
    lock sync.Mutex
)

func addWithLock() {
    for i := 0; i < 20000; i++ {
        lock.lock()
        x += 1
        lock.UnLock()
    }
}

func addWithoutLock() {
    for i := 0; i < 2000; i++ {
        x += 1
    }
}

其实通过上锁解锁的方式效率还是低了,并且容错率不高。在Go语言里面有一个 WaitGroup 是专门用于协程阻塞控制的。

WaitGroup 的效果为:在协程的计数器归零前,一直阻塞当前线程。它里面是维护了一个计数器的,如:

WaitGroup
    Add(delta int) // 计数器 + delta
    Done()         // 计数器 - 1
    Wait()         // 阻塞直到计数器为0

实例:

func ManyGoWait() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func(j int) {
            defer wg.Done()
            hello(j)
        }(i)
    }
    wg.Wait()
}