Go(5)--并发

1. 并发基础

1.1 并发和并行

  • 并行:程序在任意时刻都是同时运行的;比如说:在吃饭的时候,可以听音乐。不用停下吃饭,就可以听音乐。
  • 并发:程序在单位时间内是同时运行的;比如说:在吃饭的时候喝水,需要停下吃饭,然后去喝水。

1.2 goroutine

      操作系统可以进行线程和进程的调度,本身具备并发处理能力,但进程切换代价还是过于高昂,进程切换需要保存上下文,耗费较多的时间。而 Go 的并发基础便是基于应用程序在用户层再构筑一级调度,将并发的粒度进一步降低,避免频繁从用户态到内核态切换。

      Go 语言的并发执行体称为goroutine,通过go关键字来启动一个goroutine。注意:go关键字后面必须跟一个函数,不能是语句或其他东西,函数的返回值被忽略。

  • 通过 go + 匿名函数形式启动goroutine,代码如下:
package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	go func() {
		sum := 0
		for i := 0; i < 10000; i++{
			sum += i
		}
		fmt.Println(sum)
		time.Sleep(1 * time.Second)
	}()

	// 返回当前程序的 goroutine 数目
	fmt.Println("NumGoroutine = ", runtime.NumGoroutine())

	// main goroutine sleep 5 秒,防止其提前退出
	time.Sleep(5 * time.Second)
}

在这里插入图片描述

  • 通过 go + 有名函数形式启动goroutine,代码如下:
package main

import (
	"fmt"
	"runtime"
	"time"
)

func sum() {
	sum := 0
	for i := 0; i < 10000; i++{
		sum += i
	}
	fmt.Println(sum)
	time.Sleep(1 * time.Second)
}

func main() {
	go sum()

	// 返回当前程序的 goroutine 数目
	fmt.Println("NumGoroutine = ", runtime.NumGoroutine())

	// main goroutine sleep 5 秒,防止其提前退出
	time.Sleep(5 * time.Second)
}

在这里插入图片描述

      goroutine 有如下特性

  • go 的执行是非阻塞的,不会等待。
  • go 后面的函数的返回值会被忽略。
  • 调度器不能保证多个 goroutine的执行次序。
  • 没有父子goroutine的概念,所有的goroutine是平等地调度和执行地。
  • Go 程序在执行时会单独为 main函数创建一个goroutine,遇到其他go关键字时再去创建其他的goroutine
  • Go 没有暴露 goroutine id给用户,所以不能在一个goroutine里面显式地操作另一个goroutine,不过runtime包提供了一些函数访问和设置goroutine的相关信息。

            1、func GOMAXPROCS

            func GOMAXPROCS(n int) n用来设置或查询可以并发执行的goroutine数目,n 大于1表示GOMAXPROCS值,否则表示查询当前的GOMAXPROCS值。

package main

import "runtime"

func main() {
	// 获取当前的 GOMAXPROCS 值
	println("GOMAXPROCS = ", runtime.GOMAXPROCS(0))

	// 设置 GOMAXPROCS 的值为2
	runtime.GOMAXPROCS(2)

	// 获取当前的 GOMAXPROCS 值
	println("GOMAXPROCS = ", runtime.GOMAXPROCS(0))
}

在这里插入图片描述

            2、func Goexit

            func Goexit()是结束当前goroutine的运行,Goexit在结束当前goroutine运行之前会调用当前goroutine已经注册的deferGoexit并不会产生panic,所以该goroutine defer里面的 recover调用都返回nil

            3、func Gosched

            func Gosched()是放弃当前调度执行机会,将当前goroutine放到队列中等待下次被调度。

1.3 chan

      chan是 Go 语言里面的一个关键字,是channel的简写,即通道。通道是 goroutine之间通信和同步的重要组件,Go的哲学是“不要通过共享内存来通信,而是通过通信来共享内存”,通道是 Go 通过通信来共享内存的载体。

      channel的介绍
            1、channel 本质就是一个数据结构-队列
            2、数据是先进先出
            3、线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的。
            4、channel 是有类型的,一个 string 的channel 只能存放 stringg 类型数据。

      通道是有类型的,可以简单地把它理解为有类型地管道。声明一个简单地通道语句是chan dataType,但是简单声明一个通道变量没有任何意义,并没有被初始化,为nil。Go 提供一个内置函数make来创建通道。

// 创建一个无缓冲的通道,通道存放元素的类型为 dataType
make(chan dataType)

// 创建一个有 10个缓冲的通道,通道存放元素的类型为 dataType
make(chan dataType, 10)

      通道分为无缓冲通道和有缓冲通道,Go 提供内置函数lencap,无缓冲的通道的lencap都是0,有缓冲的通道的len代表没有被读取的元素数,cap代表整个通道的容量。无缓冲的通道既可以用于通信,也可以用于两个goroutine的同步,有缓冲的通道主要用于通信。

      在上例中,我们为了避免main goroutine过早退出,所以sleep了5秒,但是这个时间并不合理。如果另一个goroutine执行时间大于5秒,那么还未执行完便已经结束了。而如果执行时间不用5秒,显然5秒的时间依然浪费了CPU资源。而有了通道之后,便可以使用无缓冲的通道来实现goroutine之间的同步等待。

package main

import (
	"runtime"
)

func sum(c chan struct{}) {
	sum := 0
	for i := 0; i < 10000; i++{
		sum += i
	}
	println(sum)
	// goroutine 执行完后
	// 写通道
	c <- struct{}{}
}

func main() {
	c := make(chan struct{})

	go sum(c)

	// 返回当前程序的 goroutine 数目
	println("NumGoroutine = ", runtime.NumGoroutine())

	// 读通道c,通过通道进行同步等待
	<- c
}

      goroutine运行结束后退出,写道缓冲通道中的数据不会消失,它可以缓冲和适配两个goroutine处理速率不一致的情况。

package main

import "runtime"

func main() {
	var c chan struct{}
	var ci chan int

	c = make(chan struct{})
	ci = make(chan int, 100)

	go func(i chan struct{}, j chan int) {
		for i := 0; i < 10; i++{
			ci <- i
		}
		close(ci)
		c <- struct{}{}
	}(c, ci)

	println("NumGoroutine = ", runtime.NumGoroutine())

	// 读通道 c,通过通道进行同步等待
	<-c

	// 此时 ci 通道已经关闭,匿名函数启动的 goroutine 已经退出
	println("NumGoroutine = ", runtime.NumGoroutine())

	// 但通道 ci 还可以继续读取
	for v := range ci{
		println(v)
	}
}

在这里插入图片描述
      操作不同状态的chan会引发三种行为:

  • panic

            (1)向已经关闭的通道写数据会导致panic

            (2)重复关闭的通道会导致panic

  • 阻塞

            (1)向未初始化的通道写数据或读数据都会导致当前goroutine的永久阻塞。

            (2)向缓冲区已满的通道写数据会导致goroutine阻塞。

            (3)通道中没有数据,读取该通道会导致goroutine阻塞。

  • 非阻塞

            (1)读取已经关闭的通道不会引发阻塞,而是立即返回通道元素类型的零值,可以使用comma , ok语法判断通道是否已经关闭。

package main

import "fmt"

// WriteData
func WriteData(intChan chan int){
    for i := 1; i<= 50; i++{
        intChan <- i
    }
    close(intChan)
}

func ReadData(intChan chan int, exitChan chan bool){
    for{
        v, ok := <- intChan
        if !ok{
            break
        }
        fmt.Println("v = ", v)
    }
    // readData 读取完毕后,任务完成
    exitChan <- true
    close(exitChan)
}

func main()  {
    // 创建两个管道
    intChan := make(chan int, 50)
    exitChan := make(chan bool, 1)
    
    go WriteData(intChan)
    go ReadData(intChan, exitChan)
    for{
    	// ok 判断 chan 是否被关闭
        _, ok := <- exitChan
        if !ok{
            break
        }
    }
}

            (2)向有缓冲且没有满的通道读/写不会引发阻塞。

      chan使用细节和注意事项

  1. channel的遍历

            channel 支持 for - range 的方式进行遍历,注意两个细节:

            1、在遍历时,如果 channel 没有关闭,则会出现 deadlock 错误
            2、在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历.

在这里插入图片描述

  1. channel可以声明为只读、或者只写性质

在这里插入图片描述
在这里插入图片描述

  1. 使用 select 可以解决从管道取数据的阻塞问题

在这里插入图片描述
在这里插入图片描述
4. goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题.

      说明:如果我们启一个协程,但是这个协程出现了 panic,如果我们没有捕获这个 panic,就会造成整个程序崩溃,这时我们可以在 goroutine 中使用 recover 来捕获 panic,进行处理,这样即使这个协程发生问题,但是主线程依然不受影响,可以继续执行。

在这里插入图片描述在这里插入图片描述

1.4 WaitGroup

      goroutine用于并发,chan用于通信。没有缓冲的通道具有同步的功能,除此之外,sync包也提供了多个goroutine同步机制,主要通过WaitGroup实现。

      主要数据结构和操作如下:

type WaitGroup struct {
	...	
}

// 添加等待信号
func (wg *WaitGroup) Add(delta int){
	
}

// 释放等待信号
func (wg *WaitGroup) Done(){

}

// 等待
func (wg *WaitGroup) Wait(){

}

      WaitGroup用来等待多个goroutine完成,main groutine调用Add设置需要等待goroutine的数目,每一个goroutine结束时调用Done()Wait()main用来等待所有的goroutine完成。

package main

import (
	"fmt"
	"net/http"
	"sync"
)

var wg sync.WaitGroup

var urls = []string{
	"http://www.qq.org",
	"http://www.baidu.com",
}

func main() {
	for _, url := range urls{
		// 每一个 URL 启动一个 goroutine,同时给 wg 加1
		wg.Add(1)

		go func(url string) {
			// 当前 goroutine 结束后给 wg 计数减1,wg.Done() 等价于 wg.Add(-1)
			defer wg.Done()

			// 发送 HTTP get 请求,并打印 HTTP 返回码
			resp, err := http.Get(url)
			if err == nil{
				fmt.Println(resp.Status)
			}
		}(url)
	}

	// 等待所有请求结束
	wg.Wait()
}

在这里插入图片描述

1.5 select

      select是类UNIX系统提供的一个多路复用系统API,Go语言借用多路复用的概念,提供了select关键字,用于多路监听多个通道。当监听的通道没有状态是可读或可写的,select是阻塞的;只要监听的通道中有一个状态是可读或可写的,则select就不会阻塞,而是进入处理就绪通道的分支流程。如果监听的通道有多个可读或可写的状态,则selct随机选取一个操作。

package main

func main() {
	ch := make(chan int, 1)

	go func(chan int) {
		for{
			select {
				case ch <- 0:
				case ch <- 1:
			}
		}
	}(ch)

	for i := 0; i < 10; i++{
		println(<-ch)
	}
}

在这里插入图片描述

1.6 扇入和扇出

      扇入是指将多路通道聚合到一条通道中处理,Go 语言最简单的扇入就是使用 select聚合多条通道服务。

      扇出是指将一条通道发散到多条通道中处理,在 Go 语言里面具体实现就是使用 go关键字启动多个goroutine并发处理。

1.7 通知退出机制

      读取已经关闭的通道不会引起阻塞,也不会导致panic,而是立即返回该通道类型的零值。关闭select监听的某个通道能使select立即感知这种通知,然后进行相应的处理,这就是退出通知机制。

      下游的消费者不需要随机数时,显式地通知生产者停止生产。

package main

import (
	"fmt"
	"math/rand"
	"runtime"
)

// GenerateIntA 是一个随机数生成器
func GenerateIntA(done chan struct{}) chan int{
	ch := make(chan int)
	go func() {
		Label:
			for{
				select {
					case ch <- rand.Int():

					// 增加一路监听,就是对退出通知信号 done 地监听
					case <- done:
						break Label
				}
			}
		// 收到通知后关闭通道 ch
		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())
}

在这里插入图片描述

2. 并发范式

2.1 生成器

      有如下一个应用常营,就是调用一个统一地全局的生成器服务,用于生成全局事务号、订单号、序列号和随机数等。

  1. 最简单的带缓冲的生成器
package main

import (
	"fmt"
	"math/rand"
)

func GeneratorIntA() chan int  {
	ch := make(chan int, 10)

	// 启动一个 goroutine 用于生成一个随机数,函数返回一个通道用于获取随机数
	go func() {
		for  {
			ch <- rand.Int()
		}
	}()
	
	return ch
}

func main() {
	ch := GeneratorIntA()
	fmt.Println(<- ch)
	fmt.Println(<- ch)
}

  1. 多个goroutine增强型生成器
package main

import (
	"fmt"
	"math/rand"
)

func GeneratorIntA() chan int  {
	ch := make(chan int, 10)

	// 启动一个 goroutine 用于生成一个随机数,函数返回一个通道用于获取随机数
	go func() {
		for  {
			ch <- rand.Int()
		}
	}()

	return ch
}

func GeneratorIntB() chan int  {
	ch := make(chan int, 10)

	// 启动一个 goroutine 用于生成一个随机数,函数返回一个通道用于获取随机数
	go func() {
		for  {
			ch <- rand.Int()
		}
	}()

	return ch
}

func GeneratorInt() chan int  {
	ch := make(chan int, 20)

	// 使用 select 的扇入计数增加生成的随机源
	go func() {
		for  {
			select {
				case ch <- <- GeneratorIntA():
				case ch <- <- GeneratorIntB():
			}
		}
	}()

	return ch
}



func main() {
	ch := GeneratorInt()
	fmt.Println(<- ch)
	fmt.Println(<- ch)
}

  1. 有时希望生成器能够自动退出,可以借助Go通道的退出通知机制实现。
package main

import (
	"fmt"
	"math/rand"
)

func GeneratorIntA(done chan struct{}) chan int  {
	ch := make(chan int, 10)

	go func() {
		label:
		for  {
			// 通过 select 监听一个信号 chan 来确定是否停止生成
			select {
				case ch <- rand.Int():

				case <- done:
					break label
			}
		}
		close(ch)
	}()

	return ch
}

func main() {
	done := make(chan struct{})
	ch := GeneratorIntA(done)
	fmt.Println(<- ch)
	fmt.Println(<- ch)

	// 不再需要生成器,通过 close chan 发送一个通知给生成器
	close(done)

	for v := range ch{
		println(v)
	}
}

  1. 一个融合了并发、缓冲、退出通知等多重特性的生成器。
package main

import (
	"math/rand"
)

func GeneratorIntA(done chan struct{}) chan int  {
	ch := make(chan int, 10)

	go func() {
		label:
		for  {
			// 通过 select 监听一个信号 chan 来确定是否停止生成
			select {
				case ch <- rand.Int():

				case <- done:
					break label
			}
		}
		close(ch)
	}()

	return ch
}

func GeneratorIntB(done chan struct{}) chan int  {
	ch := make(chan int, 10)

	go func() {
	label:
		for  {
			// 通过 select 监听一个信号 chan 来确定是否停止生成
			select {
			case ch <- rand.Int():

			case <- done:
				break label
			}
		}
		close(ch)
	}()

	return ch
}

// 通过 select 执行扇入操作
func GeneratorInt(done chan struct{}) chan int  {
	ch := make(chan int, 20)
	// 使用 select 的扇入计数增加生成的随机源
	go func() {
		Label:
		for  {
			select {
				case ch <- <- GeneratorIntA(done):
				case ch <- <- GeneratorIntB(done):
			    case <- done:
			    	break Label
			}
		}
		close(ch)
	}()
	return ch
}



func main() {
	// 创建一个作为接收退出信号的 chan
	done := make(chan struct{})
	
	// 启动生成器
	ch := GeneratorInt(done)

	// 获取生成器资源
	for i := 0; i < 10; i++{
		println(<- ch)
	}


	// 通知生成者停止生产
	done <- struct{}{}
	println("stop generator")
}

2.2 管道

      通道可以分为两个方向,一个是读,另一个是写,假如一个函数的输入参数和输出参数都是相同的chan类型,则该函数可以调用自己,最终形成一个调用链。多个具有相同参数类型的函数也能组成一个调用链,像 UNIX 系统的管道,是一个由类型的管道。

package main

import "fmt"

// chain 函数的输入参数和输出参数类型相同,都是 chan int 类型
// chain 函数的功能是将 chan 内的数据统一加1
func chain(in chan int) chan int{
	out := make(chan int)
	go func() {
		for v := range in{
			out <- v + 1
		}
		close(out)
	}()
	return out
}

func main() {
	in := make(chan int)

	// 初始化输入参数
	go func() {
		for i := 0; i < 10; i++{
			in <- i
		}
		close(in)
	}()

	// 连续调用 3 次 chan,相当于 in 中的每个元素都加3
	out := chain(chain(chain(in)))
	for v := range out{
		fmt.Println(v)
	}
}

2.3 每个请求一个 goroutine

       为每一个请求或任务创建一个 goroutine去处理,典序场景如 Go 中的 Http Server 服务。
在这里插入图片描述

       计算 100 个自然数的和,将计算任务拆分为多个task,每个task启动一个goroutine进行处理。

package main

import (
	"fmt"
	"sync"
)

// 工作任务
type task struct {
	begin int
	end int
	result chan <- int
}

// 任务执行,计算 begin 到 end 的和
// 执行结果写入结果 chan result
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 用于同步等待任务的执行
	wait := &sync.WaitGroup{}

	// 初始化 task 的 goroutine,计算 100 个自然数之和
	go InitTask(taskchan, resultchan, 100)

	// 每个 task 启动一个 goroutine 进行处理
	go DistributeTask(taskchan, wait, resultchan)

	// 通过结果通道获取结果并汇总
	sum := ProcessResult(resultchan)
	fmt.Println("sum = ", sum)
}

// 创建 task 并写入 task 通道
func InitTask(taskchan chan <- task, r chan int, p int)  {
	qu := p / 10
	mod := p % 10
	high := qu * 10
	for j := 0; j < 10; j++{
		b := j * 10 + 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 chan,每个 task 启动一个 worker goroutine 进行处理
// 并等待每个 task 运行完,关闭结果通道
func DistributeTask(taskchan <- chan task, wait *sync.WaitGroup, result chan int)  {
	for v := range taskchan{
		wait.Add(1)
		go ProcessTask(v, wait)
	}
	wait.Wait()
	close(result)
}

// goroutine 处理具体工作,并将处理结果发送到结果通道
func ProcessTask(t task, wait *sync.WaitGroup)  {
	t.do()
	wait.Done()
}

// 读取结果通道,汇总结果
func ProcessResult(result chan int)int  {
	sum := 0
	for v := range result{
		sum += v
	}
	return sum
}



      程序逻辑分析:

            (1)InitTask 函数创建 task 并发送到 task 通道。

            (2)分发任务函数 DistributeTask 为每个 task 启动一个 goroutine 处理任务,等待其处理完成,然后关闭结果通道。

            (3)ProcessResult 函数读取并统计所有的结果。

2.4 固定 worker 工作池

      在 Java 中,我们通过线程池避免频繁创建、销毁线程浪费的资源。在 Go 中,一个可以构建固定数目的 goroutine作为工作线程池。

      在上述程序中,处理主要的 main goroutine,还开启了以下几类 goroutine:

  1. 初始化任务的goroutine
  2. 分发任务的goroutine
  3. 等待所有worker结束通知,然后关闭结果通道的goroutine

      main函数负责拉起上述goroutine,并从结果通道获取最终的结果。

      程序采用3个通道,分别是:

  1. 传递 task 任务的通道
  2. 传递 task 结果的通道
  3. 接受 worker 处理完任务后所发送通知的通道
package main

import "fmt"

// 工作池的 goroutine 数目

const (
	NUMBER = 10
)

// 工作任务
type task struct {
	begin int
	end int
	result chan <- int
}

// 任务处理:计算 begin 到 end 的和
// 执行结果写入结果 chan result
func (t *task)do()  {
	sum := 0
	for i := t.begin; i <= t.end; i++{
		sum += i
	}
	t.result <- sum
}

func main() {
	workers := NUMBER

	// 工作通道
	taskchan := make(chan task, 10)
	// 结果通道
	resultchan := make(chan int, 10)

	// worker 信号通道
	done := make(chan struct{}, 10)

	// 初始化 task 的 goroutine,计算 100 个自然数之和
	go InitTask(taskchan, resultchan, 100)

	// 分发任务到NUMBER个 goroutine 池
	DistributeTask(taskchan, workers, done)

	// 获取各个 goroutine 处理完任务的通知,并关闭结果通道
	go CloseResult(done, resultchan, workers)

	// 通过结果通道获取结果并汇总
	sum := ProcessResult(resultchan)

	fmt.Println("sum = ", sum)
}

// 创建 task 并写入 task 通道
func InitTask(taskchan chan <- task, r chan int, p int)  {
	qu := p / 10
	mod := p % 10
	high := qu * 10
	for j := 0; j < 10; j++{
		b := j * 10 + 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 chan 并分发到 worker goroutine 处理,总的数量是 workers
func DistributeTask(taskchan <- chan task, workers int, done chan struct{})  {

	for i := 0; i < workers; i++{
		go ProcessTask(taskchan, done)
	}

}

// 工作 goroutine 处理具体工作,并将处理结果发送到结果 chan
func ProcessTask(taskchan <-chan task, done chan struct{})  {
	for t := range taskchan{
		t.do()
	}
	done <- struct{}{}
}

// 通过 done chan 同步等待所有 goroutine 的结束,然后关闭 chan
func CloseResult(done chan struct{}, resultchan chan int, workers int)  {
	for i := 0; i < workers; i++{
		<-done
	}
	close(done)
	close(resultchan)
}

// 读取结果通道,汇总结果
func ProcessResult(result chan int)int  {
	sum := 0
	for v := range result{
		sum += v
	}
	return sum
}

      程序逻辑分析:

            (1)构建 task 并发送到 task 通道。

            (2)分别启动 n 个工作线程,不停地从 task 通道中获取任务,然后将结果写入结果通道。如果任务通道被关闭,则负责向收敛结果的goroutine发送通知,告诉其当前worker已经完成工作。

            (3)收敛结果的goroutine接收到所有的task已经处理完毕的信号后,主动关闭结果通道。

            (4)main 函数中的 ProcessResult 函数读取并统计所有的结果。

2.5 future 模式

      编程中经常遇到在一个流程中需要调用多个子调用的情况,这些子调用相互之间没有依赖,如果串行调用,则耗时会很长(比如在用户注册后,我们需要发送邮件和短信),此时可以使用future模式。

      future模式的基本工作原理:

            (1)使用chan作为函数参数

            (2)启动goroutine调用函数

            (3)通过chan传入参数

            (4)做其他可以并行处理的事情

            (5)通过chan异步获取结果

package main

import (
	"fmt"
	"time"
)

// 一个查询结构体
// 这里的 sql 和 result 是一个简单的抽象,具体的应用可能是更复杂的数据类型
type query struct {
	// 参数 Channel
	sql chan string

	// 结果 Channel
	result chan string
}

// 执行 Query
func execQuery(q query)  {
	// 启动协程
	go func() {
		// 获取输入
		sql := <- q.sql

		// 方法数据库

		// 输入结果通道
		q.result <- "result from " + sql
	}()
}

func main() {
	// 初始化 Query
	q := query{make(chan string, 1), make(chan string, 1)}

	// 执行 Query
	go execQuery(q)

	// 发送参数
	q.sql <- "slect * from table"

	// 做其他事情,通过 sleep 描述
	time.Sleep(1 * time.Second)

	// 获取结果
	fmt.Println(<-q.result)
}

      future模式最大的好处是将函数的同步调用转化为异步调用。

3. context 标准库

      多个goroutine之间的协作工作涉及通信、同步、通知和退出四个方面。

      通信:chan通道当然是goroutine之间通信的基础,这里的通信主要是指程序的数据通道。

      同步:不带缓冲的chan提供了一个天然的同步等待机制;当然sync.WaitGroup也为多个goroutine协同工作提供了一种同步等待机制。

      通知:通知通常不是业务数据,而是管理、控制流数据。通过在输入端绑定两个chan,一个用于业务流数据,另一个用于异常通知数据,然后通过select收敛进行处理。

      退出:goroutine 之间没有负责关系,如果通知goroutine退出,可以通过增加一个单独的通道,借助通道和select的广播机制实现退出。

3.1 context 的设计目的

      context 库的设计目的及时跟踪goroutine调用树,并在这些goroutine调用树中传递通知和元数据。两个目的:

  1. 退出通知机制 ---- 通知可以传递给整个goroutine调用树上的每一个goroutine.
  2. 传递数据 ---- 数据可以传递给真个goroutine调用树上的每一个goroutine.

3.2 基本数据结构

      context 包的整体工作机制:第一个创建 Contextgoroutine被称为root节点。root 节点负责创建一个实现 Context 接口的具体对象,并将该对象作为参数传递到其新拉起的goroutine,下游的goroutine可以继续封装该对象,再传递到更下游的goroutineContext对象再传递的过程中最终形成一个树状的数据结构,这样通过位于root节点(树的根节点)的Context对象就能遍历整个Context对象树,通知和消息就可以通过root节点传递出去,实现上游goroutine对下游goroutine的消息传递。

3.2.1 Context 接口

      Context 是一个基本接口,所有的 Context 对象都要实现该接口,context 的使用者再调用接口中都使用Context作为参数类型。

type Context interface {
	
	// 如果 Context 实现了超时控制,则该方法返回ok true, deanline 为超时时间
	// 否则 ok 为 false
	Deadline() (deadline time.Time, ok bool)
	
	// 后端被调用的 goroutine 应该监听该方法返回的 chan,以便及时释放资源
	Done() <- chan struct{}
	
	// Done 返回的 chan 收到通知的时候,才可以访问 Err() 获知因为什么原因被取消
	Err() error
	
	// 可以访问上游的 goroutine 传递给下游 goroutine 的值
	Value(key interface{}) interface{}
	
}

3.2.2 canceler 接口

      canceler 接口是一个扩展接口,规定了取消通知的 Context 具体类型需要实现的接口。context包中的具体类型* cancelCtx* timerCtx都实现了该接口。

// 一个 context 对象如果实现了 canceler 接口,则可以被取消

type canceler interface {
	// 创建一个 cancel 接口实例的 goroutine 调用 cancel 方法通知后续创建的 goroutine 退出
	cancel(removeFromParent bool, err error)
	
	// Done 方法返回的 chan 需要后端 goroutine来监听,以便及时退出
	Done() <-chan struct{}
}

3.2.3 empty Context 结构

      emptyCtx实现了 Context接口,但不具备任何功能,因为其所有的方法都是空实现。其存在的目的是作为 Context对象树的根节点。

// emptyCtx 实现了 Context 接口

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}

func (*emptyCtx) Done() <-chan struct{} {
	return nil
}

func (*emptyCtx) Err() error {
	return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}

func (e *emptyCtx) String() string {
	switch e {
	case background:
		return "context.Background"
	case todo:
		return "context.TODO"
	}
	return "unknown empty Context"
}

      package 定义了两个全局遍历和两个封装函数,返回两个 empryCtx实例对象,实际使用时通过调用这两个封装函数来构造Contextroot节点。

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

3.2.4 cancelCtx

      cancelCtx是一个实现了Context接口的具体类型,同时实现了conceler接口。conceler具有退出通知方法。退出通知机制不但能通知自己,也能逐层通知其children节点。

// cancelCtx 可以被取消,cancelCtx 取消时会同时需要所有实现 canceler 接口的孩子节点
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     chan struct{}         // created lazily, closed by first cancel call
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
}

func (c *cancelCtx) Done() <-chan struct{} {
	c.mu.Lock()
	if c.done == nil {
		c.done = make(chan struct{})
	}
	d := c.done
	c.mu.Unlock()
	return d
}

func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}

func (c *cancelCtx) String() string {
	return contextName(c.Context) + ".WithCancel"
}

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	if c.done == nil {
		c.done = closedchan
	} else {
		// 显式通知自己
		close(c.done)
	}
	// 循环调用 children 的 cancel 函数,由于 parent 已经去洗洗奥,所以此时 child 调用
	// cancel 传入的是false
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}

3.2.5 timerCtx

      timerCtx是一个实现了Context接口的具体类型,内部封装了cancelCtx类型实例,同时有一个 deadline变量,用来实现定时退出通知。

type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}

func (c *timerCtx) String() string {
	return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
		c.deadline.String() + " [" +
		time.Until(c.deadline).String() + "])"
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

3.2.6 valueCtx

      valueCtx是一个实现了Context接口的具体类型,内部封装了Context接口类型,同时封装了一个k/v的存储变量。valueCtx可用来传递通知信息。

type valueCtx struct {
	Context
	key, val interface{}
}

func (c *valueCtx) String() string {
	return contextName(c.Context) + ".WithValue(type " +
		reflectlite.TypeOf(c.key).String() +
		", val " + stringify(c.val) + ")"
}

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

3.3 API 函数

      下面这两个函数是构造 Context取消树的根节点对象,根节点对象用作后续With包装函数的实参。

func Backgroud() Context
func TODO() Context

      With包装函数用来创建不同功能的Context具体对象。

  1. 创建一个带有退出通知的Context具体对象,内部创建一个cancelCtx的类型实例。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}
  1. 创建一个带有超时通知的Context具体对象,内部创建一个timerCtx的类型实例。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}
  1. 创建一个带有超时通知的Context具体对象,内部创建一个timerCtx的类型实例。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}
  1. 创建一个能够传递数据的Context具体对象,内部创建一个valueCtx的类型实例。
func WithValue(parent Context, key, val interface{}) Context {
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

      parent参数,是实现Context通知树的必备条件。在 goroutine的调用连中,Context的实例被逐层地包装并传递,每层又可以对传进来地Context实例再封装自己所需地功能,整个调用树需要一个数据结构来维护,这个维护逻辑再这些包装函数内部实现。

3.4 辅助函数

      Context具体对象地链条关系是在 With函数的内部维护的,With函数内部使用的通用函数有如下:

func propagateCancel(parent Context, child canceler) {
	if parent.Done() == nil {
		return // parent is never canceled
	}
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
  1. 判断 parent 的方法 Done() 返回值是否是 nil,如果是,则说明 parent 不是一个可取消的 Context 对象,也就无所谓取消构造树,说明 child 就是取消构造树的根。
  2. 判断 parent 的方法 Done() 返回值不是 nil,则向上回溯自己的祖先是否是cancelCtx类型实例,如果是,则将 child 的子节点注册维护到那棵关系树里面。
  3. 如果向上回溯自己的祖先都不是cancelCtx类型实例,则说明整个链条的取消树是不连续的。此时只需要监听parent和自己的取消信号即可。
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	for {
		switch c := parent.(type) {
		case *cancelCtx:
			return c, true
		case *timerCtx:
			return &c.cancelCtx, true
		case *valueCtx:
			parent = c.Context
		default:
			return nil, false
		}
	}
}
  • 判断 parent 中是否封装了*cancelCtx的字段,或者接口里面存放的底层类型是否是* cancelCtx类型。
func removeChild(parent Context, child canceler) {
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
	p.mu.Lock()
	if p.children != nil {
		delete(p.children, child)
	}
	p.mu.Unlock()
}
  • 如果 parent封装了*cancelCtx类型字段,或者接口里面存放的底层类型是*cancelCtx类型,则将其构造树上的child节点删除。

3.5 context 的用法

package main

import (
	"context"
	"fmt"
	"time"
)

type otherContext struct {
	context.Context
}

func main() {
	// 使用 context.Background() 构建一个 WithCancel 类型的上下文
	ctxa, cancel := context.WithCancel(context.Background())

	// work 模拟运行并检测前端的退出通知
	go work(ctxa, "work1")

	// 使用 WithDeadline 包装前面的上下文对象 ctxa
	tm := time.Now().Add(3 * time.Second)
	ctxb, _ := context.WithDeadline(ctxa, tm)

	go work(ctxb, "work2")

	// 使用 WithValue 包装前面的上下文对象 ctxb
	oc := otherContext{ctxb}
	ctxc := context.WithValue(oc, "key", "andes,pass from main")

	go workWithValue(ctxc, "work3")

	// sleep 10 秒,让 work2、work3 超时退出
	time.Sleep(10 * time.Second)

	// 显式调用 work1 的 cancel 方法通知其退出
	cancel()

	// 等待 work1 打印退出信息
	time.Sleep(5 * time.Second)
	fmt.Println("main stop")
}

func work(ctx context.Context, name string)  {
	for{
		select {
			case <- ctx.Done():
				fmt.Printf("%s get msg to cancel\n", name)
				return
			default:
				fmt.Printf("%s is running\n", name)
				time.Sleep(1 * time.Second)
		}
	}
}

// 等待前端的退出通知,并试图获取 Context 传递的数据
func workWithValue(ctx context.Context, name string)  {
	for{
		select {
			case <- ctx.Done():
				fmt.Printf("%s get msg to cancel\n", name)
				return
			default:
				value := ctx.Value("key").(string)
				fmt.Printf("%s is running value = %s\n", name, value)
				time.Sleep(1 * time.Second)
		}
	}
}

// work3 在运行中能够获取前端传递过来的参数 key
work3 is running value = andes,pass from main
work1 is running
work3 is running value = andes,pass from main
work1 is running
work2 is running
work3 is running value = andes,pass from main
work2 is running
work1 is running
// work3 超时退出
work3 get msg to cancel
work1 is running
// work2 超时退出
work2 get msg to cancel
work1 is running
work1 is running
work1 is running
work1 is running
work1 is running
work1 is running
// work1 被显式的通知退出
work1 get msg to cancel
main stop

      程序分析

      在使用 Context 的过程中,程序在底层实际上维护了两条关系链,两条引用关系链如下。

      (1)children key 构成从根到叶子Context实例的引用关系,这个关系在调用With函数时进行维护,程序有一层这样呢的树状结构:

ctxa.children ---> ctxb
ctxb.children ---> ctxc

      取消通知沿着这条链从根节点向下层节点逐层广播,当然也可以在任意一个子树上调用取消通知,一样会扩散到整棵树。ctxa收到退出通知,会通知其绑定的work1,通知会广播给ctxbctxc绑定的work2work3。同理,ctxc收到退出通知,会通知到其绑定的work2,同时会广播给ctxc绑定的work3

      (2)在构造 Context的对象中不断包裹Context实例形成一个引用关系链,这个关系链的方向是相反的,是自底向上的。

ctxc.Context --> oc
ctxc.Context.Context --> ctxb
ctxc.Context.Context.cancelCtx --> ctxa
ctxc.Context.Context.cancelCtx.Context --> new(EmptyCtx)

      这个关系链主要用来切断当前Context实例和上层Context实例之间的关系,比如ctxb调用了退出通知或定时器到期了,ctxb后续就没有必要再通知广播树上继续存在,会将自己从广播树清理掉。

      Context包的一般使用流程如下:

  1. 创建一个 Context 根对象
  2. 通过 With 函数包装上一步创建的 Context 对象,使其具有特定的功能
  3. 将上一步创建的对象作为实参传给后续启动的并发函数,每个并发函数内部可以继续使用包装函数对传进来的 Context 对象进行包装,添加自己所需的功能。
  4. 顶端的goroutine在超时后调用cancel退出通知函数,通知后端的所有goroutine释放资源
  5. 后端的goroutine通过select监听Context.Done()返回的chan,及时响应前端的goroutine的退出通知,一般停止本次处理,释放所占用的资源。

4. 并发模型

4.1 CSP 简介

      CSP基本思想:将并发系统抽象为ChannelProcess两部分,Channel用来传递消息,Process用于执行,ChannelProcess之间相互独立,没有从属关系,消息的发送和接收有严格的时序限制。Go 语言主要借鉴了ChannelProcess的概念,在 Go 中Channel就是通道,Process就是goroutine

4.2 调度模型

      应用程序的并发模型是多种的,有三种:

  • 多进程模型

            进程都能多核 CPU 并发调度,优点是每个进程都有自己独立的内存空间,隔离性好、健壮性高;缺点是进程比较重,进程的切换消耗较大,进程间的通信需要多次在内核区和用户区之间复制数据。

  • 多线程模型

            这里的多线程是指启动多个内核线程进行处理,线程的优点是通过共享内存进行通信更快捷,切换代价小;缺点是多个线程共享内存空间,极易导致数据访问混乱,某个线程误操作内存挂掉可能危及整个进程组,健壮性不高。

  • 用户级多线程模型

            用户级多进程又分两种情况,一种是M:1的方式,M个用户线程对应一个内核线程,这种情况很容易因为一个系统阻塞,导致其他用户线程都会被阻塞,不能利用机器多核的优势。还有一种模式就是M:N的方式,M个用户线程对应N个内核线程,这种模式一般需要语言运行时或库的支持,效率最高。

            协程是一种用户态的轻量级线程,协程的调度完全由用户态程序控制,协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,内个内核线程可以对应多个用户协程,当一个协程执行体阻塞了,调度器会调度另一个协程执行,最大效率地利用操作系统分给系统线程地时间片。

            好处:

  1. 控制了系统线程数,保证每个线程地运行时间片充足
  2. 调度层能进行用户态地切换,不会导致单个协程阻塞整个程序的情况,尽量减少上下文切换,提升运行效率。

4.3 并发和调度

      Go 语言在语言层面引入goroutine,有以下好处:

  1. goroutine可以在用户空间调度,避免了内核态和用户态的切换导致的成本。
  2. goroutine是语言原生支持的,提供了非常简洁的语法,屏蔽了大部分复杂底层实现。
  3. goroutine更小的栈空间允许用户创建成千上万的实例。

      Go 的调度模型重抽象出三个实体:M、P、G

4.3.1 G(Goroutine)

      G 是 Go 运行时对goroutine的抽象描述,G 中存放并发执行的代码入口地址、上下文、运行环境(管理的 P 和 M)、运行栈等执行相关的元信息。

      G 的新建、休眠、恢复、停止都受到 Go 运行时管理。Go 运行时的监控线程会监控 G 的调度,G 不会长久地阻塞系统线程,运行时的调度器会自动切换到其他 G 上继续运行。G 的新建或恢复时会添加到运行队列,等待 M 取出并运行。

4.3.2 M(Machine)

      M 代表 OS 内核线程,是操作系系统层面调度和执行的实体。M 仅负责执行,M 不停地被唤醒或创建,然后执行。M 启动时进入的是运行时的管理代码,由这段代码获取 G 和 P 资源,然后执行调度。另外,Go 语言运行时会单独创建一个监控线程,负责对程序的内存、调度等信息进行监控和控制。

4.3.3 P(Processor)

      P 代表 M 运行 G 所需要的资源,是对资源的一种抽象和管理,P 不是一段代码实体,而是一个管理的数据结构,P 主要是降低 M 管理调度 G 的复杂性,增加一个间接的控制层数据结构。把 P 看作资源,而不是处理器,P 控制 Go 代码的并行读,它不是运行实体。P 持有 G 的队列,P 可以隔离调度,解除 P 和 M 的绑定就解除了 M 对 一串 G 的调用。 P 在运行模型中只是一个数据模型,而不是程序控制模型。

       M 和 P 一起构成一个运行时环境,每个 P 有一个本地的可调度 G 队列,队列里面的 G 会被 M 依次 调度执行,如果本地队列为空了,则会去全局队列偷取一部分 G,如果全局队列也是空的,则去其他的 P 中偷取一部分 G,这就是 Work Stealing 算法的基本原理。

在这里插入图片描述
      G 并不是执行体,而是用于存放并发执行体的元信息,包括并发执行的入口函数、堆栈上下文等信息。G 由于保存的是元信息,为了减少对象的分配和回收,G 对象是可以复用的,只需将相关元信息初始化为新值即可。M 仅负责执行,M 启动时进入运行时的管理代码,这段管理代码必须拿到可用的 P 后,才能执行调度。P 的数目默认时 CPu 核心数量,可以通过 runtime.GOMAXPROCS函数设置或查询,M 和 P 的数目差不多,但运行时会根据当前的状态创建 M,M 有一个最大值上限,目前时 10000;G 与 P 是一种 M : N 的惯性系,M 可以成千上万,远远大于 N。

      m0和g0

      Go 中还有特殊的 M 和 G,它们是 m0 和 g0。m0 时候启动程序后的主线程,这个 m 对象的信息会存放在全局变量 m0 中,m0 负责执行初始化操作和启动第一个 g,之后 m0 就和其他的 M 一样了。

      每个 M 都会有一个自己的管理堆栈 g0,g0 不指向任何可指向的函数,g0 仅在 M 执行管理的调度逻辑时使用。在调度或系统调用时会切换到 g0 的栈空间,全局变量的 g0 是 m0 的 g0。

      Go 启动初始化过程

  1. 分配和检查栈空间
  2. 初始化参数和环境变量
  3. 当前运行线程标记为 m0,m0 是程序启动的主线程
  4. 调用运行时初始化函数runtime.schedinit进行初始化(主要是初始化内存空间分配器、GC、生成空闲 P 列表)
  5. 在 m0 上调度第一个 G,这个 G 运行runtime.main函数。(runtime.main()函数会拉起运行时的监控线程,然后调用main包的init()初始化函数,最后执行main函数)

      什么时候创建 M、P、G

      在程序启动过程中会初始化空闲 P 列表,P 是在这个时候被创建的,同时第一个 G 也是在初始化过程中被创建的,后续在有 go 并发调用的地方都有可能创建 G,由于 G 只是一个数据结构,并不是执行实体,所以 G 是可以被复用的。在需要 G 结构时,首先要去 P 的空闲 G 列表里面寻找已经运行结束的 goroutine,其 G 会被缓存起来。

      每个并发调用都会初始化一个新的 G 任务,然后唤醒 M 执行任务。这个唤醒不是特定唤醒某个线程去工作,而是先尝试获取当前线程 M,如果无法获取,则从全局调度的空闲 M 列表中获取可用的 M,如果没有可用的,则新建 M,然后绑定 P 和 G 进行运行。所以 M 和 P 不是一一对应的,M 是按需分配的,但是运行时会设置一个上限值(默认时 10000),超出最大值将导致程序崩溃。

      抢占调度的策略

  1. 在进入系统调用前后,各封装一层代码检测 G 的状态,当检测到当前 G 已经被监控线程抢占调度,则 M 停止执行当前 G,进行调度切换。
  2. 监控线程经过一段时间检测感知到 P 运行超过一定时间,取消 P 和 M 的关联,这也是一种更高层次的调度。
  3. 监控线程经过一段时间检测感知到 G 一直运行,超过了一定的时间,设置 G 标记,G 执行栈扩展逻辑检测到抢占标记,根据相关条件决定是否抢占调度。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值