更新系列
Go语言头秃之路(零)
Go语言头秃之路(一)
Go语言头秃之路(二)
Go语言头秃之路(三)
Go语言头秃之路(四)
Go语言头秃之路(五)
-
协程(goroutine)
- 协程特点:(面试)
- 有独立的栈空间;
- 共享程序堆空间;
- 调度由用户控制;
- 协程是轻量级的线程。
- 线程和协程
- 主线程是一个物理线程,是内核态,直接作用在CPU上,比较耗费CPU资源;
- 协程从主线程开启,是轻量级线程,是逻辑态,对资源消耗相对小;
- 如果主线程退出了,则线程即使还没有执行完毕,也会退出;(主死从随)
- golang的协程机制是重要的特点,可以轻松的开启上万个协程。(并发优势)
- 与go相比,python协程占用cpu/内存资源较少(如下程序运行时观察资源使用情况)
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func main() { // Add数量必须和Done数量相同 wg.Add(5) for i := 0; i < 5; i++ { go func(n int) { defer wg.Done() fmt.Println(n) }(i) } wg.Wait() }import asyncio async def func1(n): while True: print(n) await asyncio.sleep(1) async def main(): for i in range(100000): # 运行协程,阻塞协程直到返回结果 # await func1(i) asyncio.create_task(func1(i)) await asyncio.sleep(10) asyncio.run(main())
- MPG模式
- M:操作系统主线程(物理线程)
- P:协程执行需要的上下文(需要的资源)
- G:协程
运行状态描述:

-
设置使用逻辑CPU个数 runtime包
runtime.NumCPU():查看程序使用CPU个数,在go1.8之后默认使用多个cpu;
runtime.GOMAXPROCS(2):设置程序使用cpu个数,在go1.8之前版本设置后可以提高程序执行效率。 -
互斥锁:解决资源竞争(low版)
sync.Mutex
能不用锁尽量不用:性能损耗// 使用goroutinue计算10的阶乘 package main import ( "fmt" "sync" "time" ) var ( jiecheng = make(map[int]uint, 1) // 全局互斥锁 lock sync.Mutex ) func jc(n int) { var res uint = 1 for i := 1; i <= n; i++ { res *= uint(i) } lock.Lock() jiecheng[n] = res lock.Unlock() } func main() { for i := 1; i <= 10; i++ { go jc(i) } // 解决:fatal error: concurrent map read and map write time.Sleep(time.Second * 10) lock.Lock() for j := 1; j <= 10; j++ { fmt.Printf("%d 的阶乘为 %d\n", j, jiecheng[j]) } lock.Unlock() } -
读写锁
sync.RWMutexpackage main import ( "fmt" "sync" "time" ) var wg sync.WaitGroup // 读写锁 var rwLock sync.RWMutex func read() { defer wg.Done() rwLock.RLock() fmt.Println("开始读取...") time.Sleep(time.Second) fmt.Println("读取完成。") rwLock.RUnlock() } func write() { defer wg.Done() rwLock.Lock() fmt.Println("开始写入...") time.Sleep(time.Second) fmt.Println("写入完成。") rwLock.Unlock() } func main() { wg.Add(6) for i := 0; i < 5; i++ { go read() } go write() wg.Wait() }
-
管道(channel)
channel本质是一个数据结构 - 队列;
数据按照先进先出原则(FIFO);
管道是线程安全的,多goroutinue访问时,不需要加锁;
channel是有类型的,一个string的channel只能存放string类型数据。声明语法:var 变量名
chan数据类型
eg:var intChan chan int
channel是引用类型,必须初始化(make)才能写入数据,管道make的时候定义的数据容量不能更改
从管道取一个数据后,管道长度会 -1 ,而容量不变。
无论容量存满再放入数据还是从空管道取数据都会报死锁(deadlock!)
使用任意类型的管道,即接口interface{}需要使用其中的属性或方法时,需要先进行类型断言。
遍历管道最好使用 for v:= range channel方式,注意这里只接收一个值v
管道遍历前需要关闭管道close(channel),如果管道没有关闭,最后会报错deadlock
如果编译器发现一个管道只有写没有读,则该管道会阻塞死锁(deadlock!)
如果写管道和读管道的频率不一致,小炒面不报错。type Cat struct { Name string Age int } var intChan chan interface{} intChan = make(chan interface{}, 3) cat1 := Cat{"miaomiao", 1} // 往管道写入数据 intChan <- 6 intChan <- cat1 fmt.Printf("intChan:%v \t %T\n", intChan, intChan) fmt.Printf("len: %v, cap: %v\n", len(intChan), cap(intChan)) // 从管道读取数据,丢弃则不写变量,如<- intChan n1 := <- intChan fmt.Printf("len: %v, cap: %v\n", len(intChan), cap(intChan)) getCat := <- intChan // 这里直接取getCat.Name编译不通过 cat2 := getCat.(Cat) fmt.Println(cat2.Name) // 遍历前关闭管道,管道关闭后不能再写入数据,但是可以读取数据 close(intChan) // 遍历管道 for v := range intChan { fmt.Println(v) }- 注意事项:
-
默认情况下,管道为双向(可读可写),对于一些特殊情况(一般在函数参数中,防止误操作)可以将管道声明为只读(receive-only 箭头在前面)或只写(send-only 箭头在后面)。
双向管道可以以参数形式传递给单项管道。
eg:只读 intChan <- chan int
只写 intChan chan <- int -
当不确定何时关闭管道时,使用
select语句intChan := make(chan int, 3) for i := 0; i < 3; i++ { intChan <- i } // defer close(intChan) stringChan := make(chan string, 6) for j := 0; j < 6; j++ { stringChan <- "hello" + fmt.Sprint(j) } // defer close(stringChan) for { select { // 使用select语句,即使管道没有关闭也不会报 deadlock case v := <- intChan: fmt.Println("从intChan取到:", v) case v2 := <- stringChan: fmt.Println("从stringChan取到:", v2) default: fmt.Println("取不到数啦。。") return } } -
context:控制主协程和子协程同步关闭
ctx, cancel := context.WithCancel(parent Context):需要手动执行cancel()
传入的是父context,当父context退出时,子context也会跟着退出.
ctx, cancel := context.WithTimeout(parent Context, timeout time.Duratio):指定超时时间后自动关闭,不需要手动执行cancel(),当然也可以手动执行。
ctx, cancel := context.WithDeadline(parent Context, d time.Time):到了哪个时间自动退出。
在web开发中比较常用。package main import ( "context" "fmt" "sync" "time" ) var wg sync.WaitGroup func cpuInfo(ctx context.Context) { defer wg.Done() ctxx, _ := context.WithCancel(ctx) go memoryInfo(ctxx) for { select { case <-ctx.Done(): fmt.Println("退出CPU监控。") return default: fmt.Println("实时CPU监控...") time.Sleep(time.Second) } } } func memoryInfo(ctxx context.Context) { defer wg.Done() for { select { case <-ctxx.Done(): fmt.Println("退出内存监控。") return default: fmt.Println("实时内存监控...") time.Sleep(time.Second) } } } func main() { wg.Add(2) // context.WithCancel()中传入的是父context,当父context退出时,子context也会跟着退出 ctx, cancel := context.WithCancel(context.Background()) go cpuInfo(ctx) time.Sleep(time.Second * 3) cancel() wg.Wait() } -
防止因为某个协程报错而整个程序异常退出:
在协程中使用defer+recover来捕获panic。// 在方法开头加入代码 defer func() { if err := recover(); err != nil { fmt.Println("发送邮件:执行报错了", err) } }()panic会引起主线程挂掉,同时会导致其他的协程都挂掉;
在父协程中无法捕获子协程中出现的异常,需要分别进行捕获。
本文深入探讨Go语言的协程特性,包括协程的轻量级线程属性、与线程的区别、Go的MPG模式、互斥锁与读写锁、管道channel的使用、并发安全与死锁预防,以及context在控制协程同步关闭中的应用。通过实例解析,帮助理解Go语言的并发优势和资源管理。
27

被折叠的 条评论
为什么被折叠?



