Go 并发编程1 | 青训营笔记

84 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天

1. goroutine: 协程(coroutine)

进程是资源分配的最小单位(用来分配内存空间);
线程是程序执行的最小单位(用来分配 CPU 时间);
协程用来精细利用线程。

协程的本质是一段包含了运行状态的程序。
协程的优势:资源利用率高、调度快速、超高并发。

  • 协程(coroutine) 是轻量级线程
  • 协程有独立的栈空间,堆空间是共享一个的
  • 多个协程可以在同一个线程上运行,也可以在多个线程上运行
  • 非抢占式多任务处理,只能由协程主动交出控制权,runtime.Gosched() 可以交出当前协程的控制权。
    goroutine 现在可以被异步抢占。因此没有函数调用的循环不再对调度器造成死锁或造成垃圾回收的大幅变慢。
  • 协程可能被调度器切换的时机:
    1. I/O、select
    2. channel
    3. 等待锁
    4. 函数调用时
    5. runtime.Gosched()

下面有一个资源 num 以及对它的操作 add100Times
1000 个协程同时对资源 num 执行操作 add100Times,并且没有做任何并发控制。

type IntegerHolder struct {
    num int
}

func (h *IntegerHolder) add100Times() {
    for i := 0; i < 100; i++ {
        h.num++
    }
}

func main() {
    holder := IntegerHolder{num: 0}

    for i := 0; i < 1000; i++ {
        go holder.add100Times()
    }

    time.Sleep(time.Millisecond)
    fmt.Println(holder.num)
}

// 91243

2. Atomic

原子操作是一种硬件层面的加锁机制,在原子操作内可以保证其他协程或线程不会访问当前变量。

  • CPU 级别支持的原子操作
  • x86 平台:给内存加锁,再操作
  • ARM 平台:先操作,如果操作失败,再重试(CAS)
type IntegerHolder struct {
    num int64
}

func (h *IntegerHolder) add100Times() {
    for i := 0; i < 100; i++ {
        atomic.AddInt64(&h.num, 1)
    }
}

func main() {
    holder := IntegerHolder{num: 0}

    for i := 0; i < 1000; i++ {
        go holder.add100Times()
    }

    time.Sleep(time.Millisecond)
    fmt.Println(holder.num)
}

// 100000

3. Locker 和 WaitGroup

type IntegerHolder struct {
    num  int
    lock sync.Locker
}

func (h *IntegerHolder) add100Times() {
    for i := 0; i < 100; i++ {
        h.lock.Lock()
        h.num++
        h.lock.Unlock()
    }
}

func main() {
    holder := IntegerHolder{num: 0, lock: &sync.Mutex{}}

    waitGroup := sync.WaitGroup{}
    for i := 0; i < 1000; i++ {
        waitGroup.Add(1)
        go func() {
            holder.add100Times()
            waitGroup.Done()
        }()
    }

    waitGroup.Wait()
    fmt.Println(holder.num) // 100000
}

sync.Mutex 锁正常模式过程:

  1. 尝试 CAS 直接加锁;
  2. 若不能直接加锁,则进行多次自旋尝试;
  3. 多次自旋尝试失败,则进入 sema 队列休眠。

它是非公平锁,某些协程休眠后被唤醒后,可能还要和刚刚到的协程继续竞争锁。即有锁饥饿问题。
为解决锁饥饿问题,当某个协程等待锁的时间超过了 1ms,锁将切换到饥饿模式。

sync.Mutex 锁饥饿模式过程:

  1. 尝试 CAS 直接加锁;
  2. 不自旋尝试,直接进入 sema 队列休眠;
  3. 被唤醒的协程将直接获取锁;
  4. sema 队列被清空后,切换回正常模式。

4. channel 管道

  • channel 的数据结构是一个队列
  • channel 是线程安全的
  • channel 是引用类型,需要 make 后才能使用
  • 当 channel 缓冲区写满之后,再次进行写操作,当前协程会被阻塞;
    当 channel 缓冲区读空之后,再次进行读操作,当前协程会被阻塞。

channel 的定义

var 变量名 chan 数据类型

  chan T   // 可接收和发送 T 类型的数据
chan<- T   // 只可发送数据到 channel 中
<-chan T   // 只可从 channel 中接受数据

channel 的创建

make(chan 数据类型) // 缺省则缓冲区大小为 0
make(chan 数据类型, 缓冲区大小)

4.1. channel 的使用

// 将数据 i 写入管道 c 中
c <- i

// 从管道 c 中取出数据并赋值给 n
n = <-c
  1. channel 的使用:
func sender(c chan int) {
    for i := 0; i < 10; i++ {
        c <- i
    }
}

func receiver(c chan int) {
    var n int
    for {
        n = <-c
        fmt.Println(n)
    }
}

func main() {
    var c chan int = make(chan int)

    go receiver(c)
    go sender(c)

    time.Sleep(2 * time.Second)
}
  1. createReceiver
func sender(c chan int) {
    for i := 0; i < 10; i++ {
        c <- i
    }
}

func receiver(c chan int) {
    var n int
    for {
        n = <-c
        fmt.Println(n)
    }
}

func createReceiver() chan int {
    var c chan int = make(chan int)
    go receiver(c)
    return c
}

func main() {
    c := createReceiver()
    go sender(c)

    time.Sleep(2 * time.Second)
}
  1. 再固定下收发方向:
// 只发
func sender(c chan<- int) {
    for i := 0; i < 10; i++ {
        c <- i
    }
}

// 只收
func receiver(c <-chan int) {
    var n int
    for {
        n = <-c
        fmt.Println(n)
    }
}

func createReceiver() chan<- int {
    // c 创建出来是双向的
    var c chan int = make(chan int)
    // 传递给 receiver 的是 只收的 <-chan
    go receiver(c)
    // 返回出去给 sender 的是 只发的 chan<-
    return c
}

func main() {
    c := createReceiver()
    go sender(c)

    time.Sleep(2 * time.Second)
}

4.2. channel 的关闭和遍历

  • 当关闭 channel 后就不能再写入数据了。写入会发生错误 panic: send on closed channel
  • 但仍然可以读取处于关闭状态的 channel 中的数据。
    读完了所有数据后再读也不会发生 deadlock 错误(因为管道已经关闭了),将收到零值数据(其实就是没收到数据)。
c := make(chan int, 2)
c <- 1
c <- 2

close(c)
c <- 3    // panic: send on closed channel
c := make(chan int, 2)
c <- 1
c <- 2

close(c)
fmt.Println(<-c) // 1
fmt.Println(<-c) // 2
fmt.Println(<-c) // 0
fmt.Println(<-c) // 0

遍历

<-c 还会返回第二个参数,表示管道是否有数据。
因此可以这样遍历管道:

c := make(chan int, 2)
c <- 1
c <- 2

close(c)

for data, ok := <-c; ok; {
    fmt.Println(data)
    data, ok = <-c
}
// 1
// 2

上述遍历以及下面的 for-range 遍历,若没有关闭管道,则会发生 deadlock 错误

channel 还支持 for-range 遍历:

c := make(chan int, 2)
c <- 1
c <- 2

close(c)

for data := range c {
    fmt.Println(data)
}
// 1
// 2

4.3. channel 实现 Lock

type SimpleLock struct {
    c chan struct{}
}

func NewSimpleLock() *SimpleLock {
    return &SimpleLock{
        c: make(chan struct{}, 1),
    }
}

func (lock *SimpleLock) Lock() {
    lock.c <- struct{}{}
}

func (lock *SimpleLock) Unlock() {
    <-lock.c
}
type IntegerHolder struct {
    num  int
    lock SimpleLock
}

func (h *IntegerHolder) add100Times() {
    for i := 0; i < 100; i++ {
        h.lock.Lock()
        h.num++
        h.lock.Unlock()
    }
}

func main() {
    holder := IntegerHolder{num: 0, lock: *NewSimpleLock()}

    waitGroup := sync.WaitGroup{}
    for i := 0; i < 1000; i++ {
        waitGroup.Add(1)
        go func() {
            holder.add100Times()
            waitGroup.Done()
        }()
    }

    waitGroup.Wait()
    fmt.Println(holder.num) // 100000
}