Go并发编程

Posted ryan16231112

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go并发编程相关的知识,希望对你有一定的参考价值。

并发编程

Goroutine

Goroutine是Go语言特有的并发体,是一种轻量级的线程,由go关键字启动。在真实的Go语言的实现中,goroutine和系统线程也不是等价的。

一个Goroutine会以一个很小的栈启动(可能是2KB或4KB),当遇到深度递归导致当前栈空间不足时Goroutine会根据需要动态地伸缩栈的大小(主流实现中栈的最大值可达到1GB)。因为启动的代价很小,所以我们可以轻易地启动成千上万个Goroutine。

Go的运行时还包含了其自己的调度器,这个调度器使用了一些技术手段,可以在n个操作系统线程上多工调度m个Goroutine。Go调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的Goroutine。

Goroutine采用的是半抢占式的协作调度,只有在当前Goroutine发生阻塞时才会导致调度;同时发生在用户态,调度器会根据具体函数只保存必要的寄存器,切换的代价要比系统线程低得多。运行时有一个 runtime.GOMAXPROCS 变量,用于控制当前运行正常非阻塞Goroutine的系统线程数目。

锁操作

一般情况下,原子操作都是通过“互斥”访问来保证的,通常由特殊的CPU指令提供保护。当然,如果仅仅是想模拟下粗粒度的原子操作,我们可以借助于 sync.Mutex 来实现:

import "sync"

var total struct {
    sync.Mutex
    value int
}

func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i <= 100; i++ {
        total.Lock()
        total.value += i
        total.Unlock()
    }
}

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go worker(&wg)
    go worker(&wg)
    wg.Wait()
    fmt.Println(total.value)
}

类似于java中的多线程安全类,go中也有类似的:

import (
    "sync"
    "sync/atomic"
)
var total uint64
func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    var i uint64
    for i = 0; i <= 100; i++ {
        atomic.AddUint64(&total, i)
    }
}

同步

同一个Goroutine线程内部,顺序一致性内存模型是得到保证的。但是不同的Goroutine之间,并不满足顺序一致性内存模型,需要通过明确定义的同步事件来作为同步的参考。如果两个事件不可排序,那么就说这两个事件是并发的。为了最大化并行,Go语言的编译器和处理器在不影响上述规定的前提下可能会对执行语句重新排序(CPU也会对一些指令进行乱序执行)。

func main() {
    go println("你好, 世界")
}

根据Go语言规范, main 函数退出时程序结束,不会等待任何后台线程。因为Goroutine的执行和 main 函数的返回事件是并发的,谁都有可能先发生,所以什么时候打印,能否打印都是未知的。

解决问题的办法就是通过同步原语来给两个事件明确排序:

func main() {
    done := make(chan int)
    
    go func(){
        println("你好, 世界")
        done <- 1
    }()
    
    <-done
}

当 <-done 执行时,必然要求 done <- 1 也已经执行。根据同一个Gorouine依然满足顺序一致性规则,我们可以判断当 done <- 1 执行时, println("你好, 世界") 语句必然已经执行完成了。因此,现在的程序确保可以正常打印结果。

channel通信

Channel通信是在Goroutine之间进行同步的主要方法。在无缓存的Channel上的每一次发送操作都有与其对应的接收操作相配对,发送和接收操作通常发生在不同的Goroutine上(在同一个Goroutine上执行2个操作很容易导致死锁)。无缓存的Channel上的发送操作总在对应的接收操作完成前发生.

func main() {
    // 定义管道
    done := make(chan int)
    
    go func(){
        println("你好, 世界")
        // 向管道发送数据
        done <- 1
    }()
    // 从管道获取数据
    <-done
}

以上是关于Go并发编程的主要内容,如果未能解决你的问题,请参考以下文章

云原生时代崛起的编程语言Go并发编程实战

[Go] 通过 17 个简短代码片段,切底弄懂 channel 基础

通过 SingleFlight 模式学习 Go 并发编程

Go笔记(十五):并发编程

[Go] 并发和并行的区别

全栈编程系列SpringBoot整合Shiro(含KickoutSessionControlFilter并发在线人数控制以及不生效问题配置启动异常No SecurityManager...)(代码片段