并发基础
并行:时刻同时运行
并发:单位时间内同时运行
并行时操作系统解决,并发由app解决
goroutine
Go例程,类似于线程
通过go+匿名函数启动goroutine
go func() {
sum:=0
for i :=0; i<1000; i++{
sum+=1
}
println(sum)S
time.Sleep(1*time.Second)
}()
println("NumGoroutine=", runtime.NumGoroutine())
time.Sleep(5*time.Second)
通过go+有名函数启动goroutine
//定义sum同上
func main(){
go sum()
...
}
gotoutine特点:
- go执行时非阻塞的,不会等待
- go后面的函数返回值被忽略
- 不能保证多个goroutine的执行次序
- 没有父子goroutine的概念,所有例程平等
- go执行时为main创建一个goroutine
- 不能在一个goroutine里显式调用操作另一个goroutine,但是可以查看信息
func GOMAXPROCS(n int)
用来查询可以并发执行的goroutine数目,n>1表示设置,否则表示查询
runtime.GOMAXPROCS(2)
func Goexit
结束当前goroutine的运行,结束前会调用defer,不会产生panic
func Gosched
放弃当前调度执行机会,等待下一次执行
只有goroutine不够,多个goroutine之间还要通信,协同,后面介绍
chan
关键字
通道时goroutine之间通信的重要组件
Go不需要共享内存来通信,而是通过通信来共享内存
通道可以理解为 有类型的管道
make(chan datatype)//无缓冲通道,len,cap都是0
make(chan datatype,10)//创建一个有10个缓存的通道
len:没有被读取的元素数
cap:通道容量
无缓存通道用于:通信,以及两个make(chan datatype,10)的同步
有缓存通道用于:通信
waitgroup
157
sync包提供了多个goroutine同步
wait := &sync.WaitGroup{}
//或者 var wait sync.WaitGroup 新建一个结构
wait.Add(1)
wait.Done()
wait.Wait()
举例:
主程序读取切片,goroutine对切片处理,则:
for _,url := rangeurls{
wait.Add(1)
go func(url string){
defer wait.Done()
//do sth
}(url)
}
wait.Wait()
通知退出机制
是后面context的基础
select用法,done的用法:https://www.cnblogs.com/dqh123/p/9481836.html
下游的消费者不需要随机数时,显式地通知生产者停止生产。
func GenerateIntA(done chan struct{}) chan int{
ch:= make(chan int)
go func(){
Label:
for{
select {
case ch <- rand.Int():
case <-done:
break Label
}
}
close(ch)
}()
return ch
}
func main(){
done :=make(chan struct{})
ch := GenerateIntA(done)
fmt.Println(<-ch)
fmt.Println(<-ch)
close(done)
fmt.Println(<-ch)
fmt.Println(<-ch)
println("NumGoroutine=",runtime.NumGoroutine())
}
并发范式
每个范式是一个例子,模板,可以修改后用起来
生成器
生成器可以生成 全局事务号,订单号,随机数,序列号
- 上面的代码就是例子
- 增强型生成器:
//编写两个生成器,然后用扇入技术
func GenerateInt() chan int{
ch := make(chan int,20)
go func{
for{
select{
case ch<-<-GenerateIntA:
case ch<-<-GenerateIntB:
}
}
}()
return ch
}
func main(){
ch := GenerateInt()
for i :=0;i<100;i++{
fmt.Println(<-ch)
}
}
- 可以加入并发,缓冲,退出通知等多重特性
管道
通道有两个方向:读,写。
假如一个函数的输入输出都是同类型通道,它可以自己调用自己,形成调用链;或者多个相同类型的函数互相调用,也是一个调用链。就是管道
//chain()是一个函数,死循环从输入通道里读取,读不到了就关闭out,并返回out
out :=chain(chain(chain(in)))
每个请求一个goroutine
来一个请求就启动一个goroutine处理,典型的就是go的http server服务
例子:
package main
import (
"fmt"
"sync"
)
type task struct{
begin int
end int
result chan<- int
}
func (t *task) do(){
sum :=0
for i:=t.begin;i<=t.end;i++{
sum+=i
}
t.result <-sum
}
func main(){
taskchan:= make(chan task,10)
resultchan:= make(chan int,10)
wait :=&sync.WaitGroup{}
go Inittask(taskchan, resultchan, 10)
go Distribute(taskchan, wait, resultchan)
sum:= ProcessResult(resultchan)
fmt.Println("sum=",sum)
}
//创建task并发送到task通道中,第一个task:{11,20,r},第二个:{21,30,r}
func Inittask(taskchan chan task, r chan int, p int) {
qu:=p/10
mod :=p%10
high := qu*10
for j :=0; j<qu; j++{
b := 10*j+1
e :=10*(j+1)
tsk := task{
begin: b,
end: e,
result: r,
}
taskchan<- tsk
}
if mod!=0{
tsk:= task{
begin:high+1,
end:p,
result:r,
}
taskchan<- tsk
}
close(taskchan)
}
//为每个task启动一个goroutine,等处理完成后关闭结果通道
func Distribute(taskchan chan task, wait *sync.WaitGroup, result chan int) {
for v := range taskchan{
wait.Add(1)
go ProcessTask(v,wait)
}
wait.Wait()
close(result)
}
func ProcessTask(t task, wait *sync.WaitGroup) {
t.do()
wait.Done()
}
//读取统计所有结果
func ProcessResult(resultchan chan int) int {
sum:=0
for r := range resultchan{
sum+=r
}
return sum
}
固定worker工作池
服务器编程中,用线程池提升服务的并发处理能力。
go也可以构建固定数目的goroutine工作线程池。
例子:计算整数和
涉及到的goroutine:
- main的goroutine
- 初始化任务的
- 分发任务的
- 等待所有worker结束,关闭通道的
涉及到的通道:
- 传递task任务的通道
- 传递task结果的通道
- 接收worker处理完任务后所发送通知的通道done
例子略过,和上面的类似.
future模式
在一个流程中需要调用多个子调用的情况,子调用没有依赖,一个个调用费时。
方法:
- 使用chan作为函数参数
- 启动goroutine调用函数
- 通过chan传入参数
- 做其他可以并行处理的事情
- 通过chan异步获取结果
好处是把同步调用转换为异步调用。
type query struct{
sql chan string
result chan string
}
func execquery(q query){
go func(){
sql:= <-q.sql
q.result<-"result from "+sql
}()
}
func main(){
q := query{make(chan string, 1), make(chan string,1)}
go execquery(q)
q.sql <-"select * from table"
time.Sleep(1*time.Second)
fmt.Println(<-q.result)
}
context标准库
多个goroutie如何协同工作?
通信:chan
同步:不带缓冲的chan,sync.WaitGrooup
通知:管理控制流数据,用两个chan,一个用于业务流数据,一个用于异常,然后用select收敛,这种方法不是通用的方法
退出:增加一个单独的通道,用select的广播机制退出
goroutine都是平级的,没有父子关系,但是一个go调用另一个go,再调用另一个go,其实是树状结构,怎样把树上的所有goroutine退出呢?用context
context提供两个功能:退出通知,元数据传递
congtext再内部维护一个调用树。
- 退出通知机制:通知可以传递给整个goroutine调用树上的每一个goroutine
- 传递数据:数据可以传递给整个goroutine调用树上的每一个goroutine
基本数据结构
root节点负责创建一个实现context接口的对象,并将对象作为参数传递到拉起的goroutine,下游的goroutine可以封装该对象,再传递到更下游
最后可以通过root节点遍历整个context树,消息和通知可以从root节点传递出去。
context接口:
//方法
Deadline()(deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
canceler接口:
扩展接口,规定了取消通知的context类型需要实现的接口
cancel(removeFromParent bool, err error)
Done() <-chan struct{}