Golang并发编程goroutine、channel、锁、Sync

38 阅读7分钟

并发概述

  • 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
  • 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
  • 一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。
  • 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
  • 线程:一个线程上可以跑多个协程,协程是轻量级的线程。

goroutine

在Go语言编程中不需要去自己写进程、线程、协程,只需要goroutine,当需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了。

只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

创建单个goroutine

package main
​
import "fmt"func hello() {
    fmt.Println("Hello Goroutine!")
}
func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
}

结果只打印了main goroutine done!,并没有打印Hello Goroutine!

在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。

当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束

可以做如下修改

func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
    time.Sleep(time.Second)
}

执行上面的代码你会发现,这一次先打印main goroutine done!,然后紧接着打印Hello Goroutine!。

首先为什么会先打印main goroutine done!是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的goroutine是继续执行的。

启动多个goroutine

var wg sync.WaitGroup
​
func hello(i int) {
    defer wg.Done() // goroutine结束就登记-1
    fmt.Println("Hello Goroutine!", i)
}
func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1) // 启动一个goroutine就登记+1
        go hello(i)
    }
    wg.Wait() // 等待所有登记的goroutine都结束
}

多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的

runtime包

  • runtime.Gosched()

让出cpu时间给其他goroutine执行

  • runtime.Goexit()

退出当前协程

参考链接https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/runtime%E5%8C%85.html

channel

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型

通道是引用类型,通道类型的空值是nil。

声明的通道后需要使用make函数初始化之后才能使用。

无缓冲的通道只有在有人接收值的时候才能发送值

ch4 := make(chan int2)
ch5 := make(chan bool)
ch6 := make(chan []int)

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}

单向通道

out chan<- int: out是一个通道,只能从外面接收数据
in <-chan int: in是一个通道,只能往外面发送数据

定时器

package main
​
import (
    "fmt"
    "time"
)
​
func subGoRoutine() {
    ticker := time.NewTicker(time.Second)
    cnt := 0
    for {
        cnt++
        fmt.Println(<-ticker.C)
        if cnt == 5 {
            ticker.Stop()
        }
    }
}
​
func main() {
    go subGoRoutine()
    for {
    }
}

并发安全和锁

https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/%E5%B9%B6%E5%8F%91%E5%AE%89%E5%85%A8%E5%92%8C%E9%94%81.html

多线程的同步与互斥(互斥锁、条件变量、读写锁、自旋锁、信号量)https://blog.csdn.net/daaikuaichuan/article/details/82950711

互斥锁

var x int64
var wg sync.WaitGroup
var lock sync.Mutex
​
func add() {
    for i := 0; i < 5000; i++ {
        lock.Lock() // 加锁
        x = x + 1
        lock.Unlock() // 解锁
    }
    wg.Done()
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

读写锁RWMutex

原理https://segmentfault.com/a/1190000041468059

Mutex在大量并发的情况下,会造成锁等待,对性能的影响比较大。

如果某个读操作的协程加了锁,其他的协程没必要处于等待状态,可以并发地访问共享变量,这样能让读操作并行,提高读性能。 RWLock就是用来干这个的,这种锁在某一时刻能由什么问题数量的reader持有,或者被一个wrtier持有。

主要遵循以下规则 :

  • 读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁。
  • 在读锁没有全部解锁的情况下,写操作会阻塞直到所有读锁解锁。
  • 写锁定的情况下,其他协程的读写都会被阻塞,直到写锁解锁。

Go语言的读写锁方法主要有下面两种

  • Lock/Unlock:针对写操作,不管锁是被reader还是writer持有,这个Lock方法会一直阻塞,Unlock用来释放锁的方法
  • RLock/RUnlock:针对读操作,当锁被reader所有的时候,RLock会直接返回,当锁已经被writer所有,RLock会一直阻塞,直到能获取锁,否则就直接返回,RUnlock用来释放锁的方法

并发读

package main
​
import (
    "fmt"
    "sync"
    "time"
)
​
func reader(rwlock *sync.RWMutex, wg *sync.WaitGroup, i int) {
    defer wg.Done()
​
    fmt.Println("reader start", i)
    rwlock.RLock()
    fmt.Println("reading", i)
    time.Sleep(1 * time.Second)
    rwlock.RUnlock()
    fmt.Println("reader done", i)
}
func main() {
    var rwlock sync.RWMutex
    var wg sync.WaitGroup
​
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go reader(&rwlock, &wg, i)
    }
    wg.Wait()
}
//output: 3的读还没结束,1和2已经开始读了
reader start 3
reading 3
reader start 2
reading 2
reader start 1
reading 1
reader done 1
reader done 3
reader done 2

并发读写

package main
​
import (
    "fmt"
    "sync"
    "time"
)var count int = 0
​
func reader(rwlock *sync.RWMutex, wg *sync.WaitGroup, i int) {
    defer wg.Done()rwlock.RLock()
    fmt.Println("reader start", i)
    time.Sleep(1 * time.Second)
    fmt.Println("reader done", i)
    rwlock.RUnlock()
}
func writer(rwlock *sync.RWMutex, wg *sync.WaitGroup, i int) {
    defer wg.Done()rwlock.Lock()
    fmt.Println("writer start", i)
    count++
    fmt.Println("writing count: ", count)
    time.Sleep(1 * time.Second)
    fmt.Println("writer done", i)
    rwlock.Unlock()
​
}
func main() {
    var rwlock sync.RWMutex
    var wg sync.WaitGroupfor i := 1; i <= 3; i++ {
        wg.Add(1)
        go writer(&rwlock, &wg, i)
    }
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go reader(&rwlock, &wg, i)
    }
    wg.Wait()
    fmt.Println("finish count: ", count)
}
//output:
reader start 3
reader done 3
writer start 2
writing count:  1
writer done 2
reader start 1
reader start 2
reader done 2
reader done 1
writer start 3
writing count:  2
writer done 3
writer start 1
writing count:  3
writer done 1
finish count:  3

在有reader时候,不会有write;

并且writer不会被reader打断

死锁场景

锁被拷贝使用

package main
​
import (
    "fmt"
    "sync"
)
​
func main() {
    var mu sync.Mutex
    mu.Lock()
    defer mu.Unlock()
    copyTest(mu)
}
​
//这里复制了一个锁,造成了死锁
func copyTest(mu sync.Mutex) {
    mu.Lock()
    defer mu.Unlock()
    fmt.Println("ok")
}

在函数外层已经加了一个Lock,在拷贝的时候又执行了一次Lock,因此这是一个永远不会获得的锁,因为外层函数的Unlock无法执行。

循环等待——哲学家就餐

A等待B,B等待C,C等待A,陷入了无限循环(哲学家就餐问题)

package main
​
import (
    "sync"
)
​
func main() {
    var muA, muB sync.Mutex
    var wg sync.WaitGroup
​
    wg.Add(2)
    go func() {
        defer wg.Done()
        muA.Lock()
        defer muA.Unlock()
        //A依赖B
        muB.Lock()
        defer muB.Lock()
    }()
​
    go func() {
        defer wg.Done()
        muB.Lock()
        defer muB.Lock()
        //B依赖A
        muA.Lock()
        defer muA.Unlock()
    }()
    wg.Wait()
}

Sync

sync.WaitGroup

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成

方法名功能
(wg * WaitGroup) Add(delta int)计数器+delta
(wg *WaitGroup) Done()计数器-1
(wg *WaitGroup) Wait()阻塞直到计数器变为0

示例

var wg sync.WaitGroup
​
func hello() {
    defer wg.Done()
    fmt.Println("Hello Goroutine!")
}
func main() {
    wg.Add(1)
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
    wg.Wait()
}

sync.Once

保证一个goroutinue只执行一次,且是线程安全的

sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

var icons map[string]image.Image
​
var loadIconsOnce sync.Once
​
func loadIcons() {
    icons = map[string]image.Image{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
    }
}
​
// Icon 是并发安全的
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}