Go并发编程总结 | 青训营

79 阅读4分钟

写在前面

并发是一个比较宽泛的概念,是指计算机能够同时执行多个任务,对于同时进行的任务数大于cpu数的情况,并发就牵扯到任务的调度问题。Java、C++等语言都支持并发编程,Go当然也不例外,同时由于go有协程,go的并发编程是有其独特之处的。

使用goroutine

goroutine是go的特性之一,可以使用关键字go快速启动并发的goroutine,语法格式为go 函数名(函数参数) 比如我们并发打印10个数字,示例函数如下

func printNum(num int) {  
    fmt.Println(num)  
}  
func main() {  
    for i := 0; i < 10; i++ {  
        go printNum(i)  
    }  
    fmt.Println("main")
    time.Sleep(time.Second)  
}

可以发现打印的顺序并不是0-10,而是错序打印的,同时可以观察到“main”的顺应也并不是固定的,即main函数也在这个调度的过程中,这展示了并发的调度,同时我们可以发现,如果没有time.Sleep语句强制停留一段时间,基本上程序会立即结束,看不到printNum函数的调用。

channel的使用

  • channel可以很容易地实现生产消费模型,同时由于channel可以分为有缓存和无缓存,有缓存和无缓存之间的主要区别在于无缓存channel读写两端只要有一方不工作,另一方就要阻塞,而有缓存channel只要有数据,两端就可以各自工作。
  • 下述程序是一个有缓冲的channel,用来快速输出0-50所有数的十倍
  • 另外非常重要的是不要忘记在结束时关闭通道资源,如果两端都不关闭且不进行操作,那么程序将会陷入死锁状态。
  • 值得一提的是,go在一定程度上是可以检测出死锁状态的,比如下述程序如果忘记close channel,那么会提示all goroutines are asleep - deadlock!,可以帮助我们发现死锁错误。
    c := make(chan int, 10)  
    d := make(chan int, 10)  
    go func() {  
    for i := 0; i < 50; i++ {  
    c <- i  
    }  
    close(c)  
    }()  
    go func() {  
    for i := range c {  
    d <- i * 10  
    }  
    close(d)  
    }()  
    for i := range d {  
    fmt.Println(i)  
    }

select的使用

  • select语句只能用于通道的操作,select会监听指定通道的操作,当有通道准备好便会指向对应的代码,如果有多个通道准备好,则随机选取一个。
  • 该程序可以随机打印c或者d或者no recv,对于select,当上述的条件都没有被执行的时候,select会执行default对应的语句。
  • 值得一提的是,如果select中有default,则执行default语句,如果没有,那么select会进行阻塞,直到有条件被触发进而执行对应的语句。
c := make(chan string)  
d := make(chan string)  
go func() {  
for {  
c <- "c"  
}  
}()  
go func() {  
for {  
d <- "d"  
}  
}()  
for {  
select {  
case msg1 := <-c:  
fmt.Println("received", msg1)  
case msg2 := <-d:  
fmt.Println("received", msg2)  
default:  
fmt.Println("no recv")

锁的使用

锁是保证并发安全的过程中十分重要的一种手段,Go提供了标准库的sync锁,其保证了并发的安全性,包括了互斥锁、读写锁、并发安全字典、条件同步变量等内容

  • Mutex互斥锁

    • 互斥锁有Lock和Unlock两个方法,互斥锁的作用主要是保证某一段操作能够原子地执行,即一段操作在执行中途不会被其他工作过程打断。
    • 一个十分简单的用到锁的场景
      lock.Lock()
      x = x + 1
      lock.Unlock()
      
  • RWMutex读写锁

    • 读写锁保证只有一个一个协程在写的状态,但是可以保证多个协程在读的状态
    • 读写锁的设计保证了读的并发效率同时也保证了数据的一致性
  • WaitGroup

    • WaitGroup有Add、Done、Wait方法
    • 作用主要是等待一组goroutine执行完成,用来阻塞主程序,避免主程序执行完退出但协程还没有执行的情况
    • 可以用Add方法增加计数器,用Done方法减少计数器,Wait方法将阻塞到计数器为0

总结

并发是一种非常重要的技术,是非常复杂的,但也是与我们的实际生产分不开的,但万变不离其宗,我们了解并发的基本机制、锁的使用是我们入门并发的第一步,可以通过实际生产和相关文章的学习来精进自己的并发功力。