Coroutine

在Python中可以通过yield关键字构造一个生成器函数,使用next函数驱动它执行,当函数执行到yield就暂停执行了。

import string
import time


def count():
    c = 1
    for i in range(5):
        print(c)
        yield c
        print("******")
        c += 1


def char():
    s = string.ascii_lowercase
    for c in s:
        print(c)
        yield c


t1 = count()  # 迭代器对象
t2 = char()
tasks = [t1, t2]
while True:
    pops = []  # 待移除的已经完成的任务
    for i, task in enumerate(tasks):
        if next(task, None) is None:  # 如果迭代器到头了,就返回给定的缺省值
            print(f"任务{task}完成...")
            time.sleep(1)
            pops.append(i)  # 记录索引
    for i in reversed(pops):
        tasks.pop(i)
    print(len(tasks), tasks)
    if len(tasks) == 0:
        time.sleep(1)  # 如果任务列表为0,就等待

可以通过上面的代码看到2个任务交替进行,而这个函数的交替,完全是代码实现的,而不是靠多线程的时间片用完操作系统强行切换,而且这种切换是在同一个线程中完成的。 最重要的是协程的切换是在用户态完成,而不是像线程那样在内核态完成。所以,Coroutine是可以在用户态通过控制在适当的时机让出执行权的多任务切换技术。上例中,交替执行任务是可以由程序员在一个线程内完成,这个任务如果再被按照Python语法封装后就是Python的协程。核心点是,在适当的时候要暂停一个正在运行的任务,让出来去执行另外一个任务。 注意:只要是代码就要在线程中执行,协程也不例外。

协程弊端

一旦一个协程阻塞,那么该线程代码被阻塞不能向下继续执行了。协程必须主动让出,才能轮到该线程中另外一个协程运行。能否让协程自由的在不同线程中移动,这样就不会因为协程阻塞了某一个线程而导致该线程中其他协程得不到执行?Go语言对Coroutine做了非常多的优化,提出了Goroutine

Goroutine

GMP模型

Robert Griesemer、Rob Pike、Ken Thompson三位Go语言创始人,对新语言商在讨论时,就决定了 要让Go语言成为面向未来的语言。当时多核CPU已经开始普及,但是众多“古老”编程语言却不能很好的适应新的硬件进步,Go语言诞生之初就为多核CPU并行而设计。Go语言协程中,非常重要的就是协程调度器scheduler和网络轮询器netpoller

Go协程调度中,三个重要的角色:

  • M: Machine Thread,对系统线程抽象、封装。所有代码最终都要在系统线程上运行,协程最终也不例外。
  • G: Goroutine,Go协程。存储了协程的执行栈信息、状态和任务函数等。初始栈的大小约为2~4k,理论上开启上万个Goroutine也不是问题。
  • P: Processor,Go1.1版本引入,虚拟处理器
  • 可以通过环境变量GOMAXPROCSruntime.GOMAXPROCS()设置,默认为CPU核心数
  • P的数量决定着最大可以并行的G的数量
  • P有自己的列队(长度256)里面放着待执行的G
  • M和P需要绑定在一起,这样P列队中的G才能真正在线程上执行

Go协程GMP模型_Goroutine

过程解释:

1、使用go func()创建一个GoroutineG1

2、当前P为P1,将G1加入当前P的本地列队LRQ(Local Run Queue)。如果LRQ满了,就加入到GRQ(Global Run Queue).

3、P1和M1绑定,M1先尝试从P1的LRQ中请求G,如果没有就从GRQ中请求G,如果还没有,就随机重别的P的LRQ中偷取(work stealing)一部分G到本地LRQ中。

4、假设M1最终拿到了G1

5、执行,让G1的代码在M1线程上执行

5.1、G1正常执行完了,G1和M1解绑,执行第3步的获取下一个可执行的G

5.2、G1中代码主动让出控制权,G1和M1解绑,将G1加入到GRQ中,执行第3步的获取下一个可执行的G

5.3、G1中进行channel、互斥锁等操作进入阻塞态,G1和M1解绑,执行第3步的获取下一个可执行的G,如果阻塞态的G1被其他协程G唤醒后,就尝试加入到唤醒者的LRQ中,如果LRQ满了,就连同G和LRQ中的一半转移到GRQ中

5.4、系统调用

5.4.1、同步系统调用时:

  • 如果遇到同步阻塞系统调用,G1阻塞M1也被阻塞,M1和P1解绑。
  • 从休眠列队中获取一个空闲的线程和P1绑定,并从P1列队中获取下一个可执行的G来执行;如果休眠列队中无空闲线程,就创建一个线程M给P1使用。
  • 如果M1阻塞结束,需要和一个空闲的P绑定,优先和原来的P1绑定,如果没有空闲的P,G1会放到GRQ中,M1加入到休眠线程列队中。

5.4.2、异步网络IO调用时:

Go协程GMP模型_Goroutine_02

  • 网络IO代码会被Go在底层变成非阻塞IO,这样就可以使用IO多路复用了。
  • M1执行G1,执行过程中发生了非阻塞IO调用(读写)时,G1和M1解绑,G1会被网络轮询器Netpoller接手。M1再从P1的LRQ中获取下一个G执行,此时M1和P1不解绑。
  • G1等待的IO就绪后,G1从网络轮询器移回P1的LRQ或者全局GRQ中,重新进入可执行状态。

大致相当于网络轮询器Netpoller内部就是使用了IO多路复用和非阻塞IO,类似select循环。Go对不同的操作系统都提供支持。

Go TCP编程

package main

import (
	"fmt"
	"log"
	"net"
	"strings"
)

func main() {
	tcpAddr, err := net.ResolveTCPAddr("tcp4", "0.0.0.0:9999")
	if err != nil {
		log.Panicln(err)
	}
	tcpListener, err := net.ListenTCP("tcp4", tcpAddr)
	if err != nil {
		log.Panicln(err)
	}
	tcpConn, err := tcpListener.AcceptTCP()
	defer tcpConn.Close()
	buffer := make([]byte, 4096)
	n, err := tcpConn.Read(buffer)
	if err != nil {
		log.Panicln(err)
	}
	data := buffer[:n]
	upperData := string(data)
	fmt.Println(string(data))

	write, err := tcpConn.Write([]byte(strings.ToUpper(upperData)))
	if err != nil {
		log.Panicln(err)
	}
	fmt.Println(write)
}

Goroutine

协程创建

使用go关键字就可以把一个函数定义为一个协程。

package main

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

func add(x, y int) int {
	var c int
	defer fmt.Printf("1 return %d\n", c)
	defer func() { fmt.Printf("2 return %d\n", c) }() //闭包
	c = x + y
	return c
}
func main() {
	fmt.Println(runtime.NumGoroutine())
	fmt.Println("main start")
	go add(4, 5)
	fmt.Println(runtime.NumGoroutine())
	time.Sleep(2 * time.Second) //Sleep阻塞主线程结束
	fmt.Println("main end")
	fmt.Println(runtime.NumGoroutine())
}

//结果输出:
1
main start
2
2 return 9
1 return 0
main end
1

上述例子中,如果主线程结束那么Goroutine将自动结束,通过Sleep暴力阻塞主线程运行,其实在Go语言中提供了等待组(WaitGroup来优雅的实现这一操作,简单来说就是有几个协程就add / done几次。

等待组

参考:https://pkg.go.dev/sync#WaitGroup

使用等待组三板斧:

  1. var wg sync.WaitGroup
  2. wg.Add(1)
  3. defer wg.Done()
package main

import (
	"fmt"
	"runtime"
	"sync"
)

func add(x, y int, wg *sync.WaitGroup) int {
	defer wg.Done() //add执行完成之后计数器减1
	var c int
	defer fmt.Printf("1 return %d\n", c)
	defer func() { fmt.Printf("2 return %d\n", c) }()
	c = x + y
	return c
}
func main() {
	var wg sync.WaitGroup //定义等待组
	fmt.Println(runtime.NumGoroutine())
	fmt.Println("main start")
	wg.Add(1)         //即将启动add协程,计数器加1
	go add(4, 5, &wg) //协程
	fmt.Println(runtime.NumGoroutine())
	wg.Wait() //阻塞等待主线程 知道wg的计数为0
	fmt.Println("main end")
	fmt.Println(runtime.NumGoroutine())
}
父子协程

一个协程A中创建了另外一个协程B,A为父,B为子。

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	fmt.Println("main start")
	count := 6
	wg.Add(count)
	go func() {
		fmt.Println("父协程开始,准备创建并启动子协程")
		defer func() {
			wg.Done() //注意wg的作用域
			fmt.Println("父协程结束了***")
		}()
		for i := 0; i < count-1; i++ {
			go func(id int) {
				defer wg.Done()
				fmt.Printf("子协程 %d 运行中\n", id)
				time.Sleep(3 * time.Second)
				fmt.Printf("子协程 %d 结束了\n", id)
			}(i)
		}
	}()
	wg.Wait()
	fmt.Println("main end")
}

/*
main start
父协程开始,准备创建并启动子协程
父协程结束了***
子协程 4 运行中
子协程 2 运行中
子协程 0 运行中
子协程 1 运行中
子协程 3 运行中
子协程 3 结束了
子协程 0 结束了
子协程 4 结束了
子协程 2 结束了
子协程 1 结束了
main end
*/

从上述输出结果看,父协程结束执行,子协程不会受影响,同样,子协程结束执行也不会对父协程有影响。父子协程都是独立运行;只有主协程结束,程序结束执行。

Goroutine实现WEB Server

package main

import (
	"fmt"
	"log"
	"net"
	"strings"
)

const HTMLTEMPLATE = `<!DOCTYPE html>
<html lang="en">
<head>
		<meta charset="UTF-8">
	<title>goDBA</title>
</head>
	<body>
		<div>
		<h1>hello world!</h1>
		</div>
	</body>
</html>
`
const HEADER = `HTTP/1.1 200 OK
Date: Mon, 24 Oct 2022 20:04:23 GMT
Content-Type: text/html
Content-Length: %d
Connection: keep-alive
Server: dba.test.cn

%s
`

var response = strings.ReplaceAll(fmt.Sprintf(HEADER, len(HTMLTEMPLATE), HTMLTEMPLATE), "\n", "\r\n")

func main() {
	tcpAddr, err := net.ResolveTCPAddr("tcp4", "0.0.0.0:9000") //解析地址
	if err != nil {
		log.Panicln(err)
	}
	tcpListener, err := net.ListenTCP("tcp4", tcpAddr)
	if err != nil {
		log.Panicln(err)
	}
	defer tcpListener.Close() //关闭连接
	for {
		conn, err := tcpListener.Accept() //接受连接,分配socket
		if err != nil {
			log.Panicln(err)
			return
		}
		go func() {
			defer conn.Close()           //保证一定关闭连接
			buffer := make([]byte, 4096) //设置缓冲区
			n, err := conn.Read(buffer)  //成功返回接收了多少字节
			if err != nil {
				log.Panicln(err)
				return
			}
			if n == 0 {
				log.Printf("客户端%s主动断开", conn.RemoteAddr().String())
			}
			fmt.Println(string(buffer[:n]))
			conn.Write([]byte(response))
		}()
	}
}