雨痕 Go语言学习笔记-并发

Goroutine

  1. 通过go关键字创建并发执行单元,有调度器安排到适合的系统线程执行
  2. 调度器不能保证多个 goroutine 执⾏次序,且进程退出时不会等待它们结束
  3. 默认情况下,进程启动后仅允许⼀个系统线程服务于 goroutine。可使⽤环境变量或标准库函数runtime.GOMAXPROCS修改,让调度器⽤多个线程实现多核并⾏,⽽不仅仅是并发(注意并行和并发的区别)
  4. 调⽤runtime.Goexit将⽴即终⽌当前goroutine 执⾏,调度器确保所有已注册 defer延迟调⽤被执⾏
  5. 和协程 yield 作⽤类似, Gosched 让出底层线程,将当前 goroutine 暂停,放回队列等待下次被调度执⾏。

note:关于goroutine,雨痕大大这里讲解得比较浅,后续自己还需要深入学习。

Channel

  1. 引⽤类型 channel 是 CSP 模式的具体实现,⽤于多个 goroutine 通讯。其内部实现了同步,确保并发安全
  2. 默认为同步模式,需要发送和接收配对。否则会被阻塞,直到另⼀⽅准备好后被唤醒
func main() {
    data := make(chan int) // 数据交换队列
    exit := make(chan bool) // 退出通知
    go func() {
        for d := range data { // 从队列迭代接收数据,直到 close 。
        fmt.Println(d)
    }
    fmt.Println("recv over.")
    exit <- true // 发出退出通知。
}()

    data <- 1 // 发送数据。
    data <- 2
    data <- 3
    close(data) // 生产者显示关闭队列。
    fmt.Println("send over.")
    <-exit // exit非缓冲channel,会阻塞在这里直到等到退出通知。
}

# output
1 2 3
send over.
recv over
  1. 异步⽅式通过判断缓冲区来决定是否阻塞。如果缓冲区已满,发送被阻塞;缓冲区为空, 接收被阻塞
  2. 通常情况下,异步 channel 可减少排队阻塞,具备更⾼的效率。但应该考虑使⽤指针规

    避⼤对象拷⻉,将多个元素打包,减⼩缓冲区⼤⼩等。
  3. 缓冲区是内部属性,并⾮类型构成要素
  4. 除⽤ range 外,还可⽤ ok-idiom 模式判断 channel 是否关闭。
for {
    if d, ok := <-data; ok {
        fmt.Println(d)
    } else {
        break
    }
}
  1. 向closed channel 发送数据引发 panic 错误,接收⽴即返回零值(何解)。⽽ nil channel,⽆论收发都会被阻塞

单向channel

  1. 可以将 channel 隐式转换为单向队列,只收或只发,但是不能将单向 channel 转换为普通 channel
c := make(chan int, 3)
var send chan<- int = c // 隐式转换send-only
var recv <-chan int = c // receive-only
send <- 1
// <-send // Error: receive from send-only type chan<- int
<-recv
// recv <- 2 // Error: send to receive-only type <-chan int

channel选择

1 如果需要同时处理多个 channel,可使⽤ select 语句。它随机选择⼀个可⽤ channel 做收发操作,或执⾏ default case

package main

import "fmt"

func main() {
	select {}
	fmt.Println("Hello, 世界")
}
# output golang 1.7.4
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:
main.main()
	/playground/e55094d1cbbe98865858cb3997351c63.go:6 +0x14
  1. 在循环中使⽤ select default case 需要⼩⼼,避免形成洪⽔。会一直循环,无法跳出循环

channel常用模式

  1. ⽤简单⼯⼚模式打包并发任务和 channel。NewTest函数中简单封装了一个任务,任务结束后返回给channel,这是一种设计思想。
2. ⽤channel 实现信号量 (semaphore),不是太理解
package main

import (
	"fmt"
	"runtime"
	"sync"
)
func main() {
	runtime.GOMAXPROCS(3)
	wg := sync.WaitGroup{}
	wg.Add(3)
	sem := make(chan int, 1)
	for i := 0; i < 3; i++ {
		go func(id int) {
			defer wg.Done()
			sem <- 1 // 向 sem 发送数据,阻塞或者成功。
			for x := 0; x < 3; x++ {
				fmt.Println(id, x)
			}
			<-sem // 接收数据,使得其他阻塞 goroutine 可以发送数据。
		}(i)
	}
	wg.Wait()
}
# output
2 0
2 1
2 2
0 0
0 1
0 2
1 0
1 1
1 2
goroutine随机调度?
  1. ⽤ closed channel 发出退出通知
4. ⽤select 实现超时 (timeout) 5. channel 是第⼀类对象,可传参 (内部实现为指针) 或者作为结构成员
type Request struct {
data []int
ret chan int
}
func NewRequest(data ...int) *Request {
return &Request{ data, make(chan int, 1) }
}
func Process(req *Request) {
x := 0
for _, i := range req.data {
x += i
}
req.ret <- x
}
func main() {
req := NewRequest(10, 20, 30)
Process(req)
fmt.Println(<-req.ret)
}

总结

  1. 弄清楚CSP并发模型
  2. 同步、信号量机制等
  3. channel异步通信时发送和接收被阻塞的情况
comments powered by Disqus