d)
// 打印3次字母
for count := 0; count < 3; count++ {
for ch := 'a'; ch < 'a'+26; ch++ {
if count == 0 {
time.Sleep(10 * time.Millisecond)
}
if count == 1 {
time.Sleep(30 * time.Millisecond)
}
if count == 2 {
time.Sleep(50 * time.Millisecond)
}
fmt.Printf("%c ", ch)
}
fmt.Println()
}
}()
// 开一个go协程打印数字
go func() {
defer wg.Done()
// 打印3次数字
for count := 0; count < 3; count++ {
for n := 1; n <= 26; n++ {
if count == 0 {
time.Sleep(20 * time.Millisecond)
}
if count == 1 {
time.Sleep(40 * time.Millisecond)
}
if count == 2 {
time.Sleep(60 * time.Millisecond)
}
fmt.Printf("%d ", n)
}
fmt.Println()
}
}()
// 等待返回
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating Program")
}
看下结果:
go run main.go
Starting Goroutines
Waiting To Finish
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
1 2 3 4 5 6 7 8 9 10 11 a 12 b c d e 13 f g h i 14 j k l m 15 n o p 16 q r s t 17 u v w x 18 y z
19 a b 20 c 21 d 22 e f 23 g 24 h 25 i j 26
k l 1 m n 2 o p 3 q r 4 s t 5 u v 6 w x 7 y z
8 a 9 b 10 c 11 d 12 e f 13 g 14 h 15 i 16 j 17 k l 18 m 19 n 20 o 21 p 22 q r 23 s 24 t 25 u 26
v w x y z
Terminating Program
通过上面的结果我们可以看到,当goroutine1阻塞时,go调度器会调度goroutine2执行。
我们可以得出:
- 即使我们将 runtime.GOMAXPROCS(1) 设置为 1,程序也在并发运行
- Running 状态的 Goroutine 数量最大为 1,Block Goroutine 可以多于一个,其他所有 Goroutine 都处于 Runnable 状态
3、线程池
- 在需要时创建一个线程,这意味着如果有 goroutine 要运行但所有其他线程都忙,则创建一个线程
- 一旦线程完成其执行而不是销毁重用它
- 这可以更快的创建goroutine,因为我们可以重用线程
- 但是还有更多的内存消耗,性能问题,并且没有无限堆栈。
4、M: N 线程共享运行队列调度(GMP)
- M代表系统线程的数量
- N代表goroutine的数量
- goroutine 的创建成本很低,我们可以完全控制 goroutine 的整个生命周期,因为它是在用户空间中创建的
- 创建一个操作系统线程很昂贵,我们无法控制它,但是使用多个线程我们可以实现并行
- 在这个模型中,多个 goroutine 被多路复用到内核线程中
我们上面提到过导致goroutine阻塞调用可能是下面一些原因:
- 在channel中收发数据
- 网络IO调用
- 阻塞的系统调用
- 计时器
- 互斥操作(Mutex)
下面看一些goroutine阻塞的例子:
package main
import (
"time"
"fmt"
"sync"
"os"
"net/http"
"io/ioutil"
)
// 全局变量
var worker int
func writeToFile(wg *sync.WaitGroup,){
defer wg.Done()
file, _ := os.OpenFile("file.txt", os.O_RDWR|os.O_CREATE, 0755) // 系统调用阻塞
resp, _ := http.Get("https://blog.waterflow.link/articles/1662706601117") // 网络IO阻塞
body, _ := ioutil.ReadAll(resp.Body) // 系统调用阻塞
file.WriteString(string(body))
}
func workerCount(wg *sync.WaitGroup, m *sync.Mutex, ch chan string) {
// Lock() 给共享资源上锁
// 独占访问状态,
// 增加worker的值,
// Unlock() 释放锁
m.Lock() // Mutex阻塞
worker = worker + 1
ch <- fmt.Sprintf("Worker %d is ready",worker)
m.Unlock()
// 返回, 通知WaitGroup完成
wg.Done()
}
func printWorker(wg *sync.WaitGroup, done chan bool, ch chan string){
for i:=0;i<100;i++{
fmt.Println(<-ch) // Channel阻塞
}
wg.Done()
done <-true
}
func main() {
ch :=make(chan string)
done :=make(chan bool)
var mu sync.Mutex
var wg sync.WaitGroup
for i:=1;i<=100;i++{
wg.Add(1)
go workerCount(&wg,&mu,ch)
}
wg.Add(2)
go writeToFile(&wg)
go printWorker(&wg,done,ch)
wg.Wait()
<-done // Channel阻塞
<-time.After(1*time.Second) // Timer阻塞
close(ch)
close(done)
}
下面我们看看go调度器在上面这些例子中是如何工作的:
- 如果一个 goroutine 在通道上被阻塞,则通道有等待队列,所有阻塞的 goroutine 都列在等待队列中,并且很容易跟踪。 在阻塞调用之后,它们将被放入 schedular 的全局运行队列中,OS Thread 将再次按照 FIFO 的顺序选择 goroutine。
- M1,M2,M3尝试从全局G