Goroutine
Go 中使用 Goroutine 来实现并发 (能够简易的实现多线程)
Goroutine 是 Go的特有名词, 区别于进程Process, 线程Thread.
Goroutine 是与其他函数或方法同时进行的函数或方法。创建Goroutine的成本很小, 就是一段代码, 一个函数入口, 以及在堆上为其分配一个堆栈(初始大小为4k, 会随着程序执行自带增删) Goroutine 是竞争cpu的
如何使用?🧐 在调用函数或者方法前面加上go关键字即可~
package main
import "fmt"
func main() {
// goroutine 并发执行
go hello()
for i := 0; i < 100; i++ {
fmt.Println("main-", i) // 打印效果是交替执行~
}
}
func hello() {
for i := 0; i < 100; i++ {
fmt.Println("hello-", i)
}
}
Goroutine的相关规则
- 当新的Goroutine开始时, Goroutine调用就会返回. 与函数不同, go 不会等待Goroutine执行结束
- 当Goroutine调用, 且其返回的任何返回值都被忽略之后, go立即执行下一行代码
- 如果main的Goroutine终止, 那么程序将被终止
主Goroutine -> main
背后做的事情, 首先是设定每一个goroutine 所能申请栈空间的最大尺寸。
32位计算机 -> 250MB. 64位计算机 -> 1GB。如果申请的空间大于这个限制就会报出栈溢出 -> panic 恐慌, 随后 go程序会终止
然后会进行一系列初始化工作
- 创建一个特殊的defer语句, 用于主Goroutine退出时做一些必要的处理
- 启动专用于在后台清扫内存垃圾的Goroutine, 并设置GC可用的标识
- 执行main包中所引用包里的init函数
- 执行main函数~
runtime包
runtime包的使用 -> 能够获取系统信息
package main
import (
"fmt"
"runtime"
)
// 获取系统信息 runtime.xxx()
func main() {
// 获取goRoot目录 (常用于开源项目中~)
fmt.Println("GoRoot Path:", runtime.GOROOT())
// 获取操作系统 windows: "//" linux: '' -> 判断盘符
fmt.Println("System:", runtime.GOOS)
// 获取cpu数量 -> 可以试着进行程序优化
fmt.Println("cpu num:", runtime.NumCPU())
// runtime.Gosched() 礼让, 让出时间片 (Goroutine调度) 配合 Goroutine 使用
// runtime.Goexit() 即终止当前的Goroutine
}
多线程会遇到的问题
临界资源的安全问题
临界资源: 指的是在并发环境中, 多个进程, 线程or协程共享的资源
package main
import (
"fmt"
"time"
)
func main() {
// 临界资源
a := 1
go func() {
a = 2
fmt.Println("goroutine a:", a)
}()
a = 3
time.Sleep(time.Second * 3) // 切换 cpu调度
fmt.Println("main a:", a)
}
其中有一个多线程的经典问题 -> 售票问题
会出现临界资源争抢的问题, 那么该如何解决 ?
可以通过上锁来解决 -> 通过上锁的方式, 在某一时间段, 只能允许一个 goroutine来访问这个共享数据, 当前goroutine访问完毕, 解锁后, 其他的goroutine才能来访问
借助sync 包下的锁操作~
package main
import (
"fmt"
"sync"
"time"
)
// 定义全局变量
var ticket int = 20
// 定义锁 sync.Mutex 锁头 -> 提供上锁和解锁(这两个常用)
var mutex sync.Mutex
func main() {
// 多线程资源争取会出现问题
go saleTicket("sam")
go saleTicket("lo")
go saleTicket("a")
go saleTicket("b")
time.Sleep(time.Second * 3)
}
// 售票
// 为了解决多进程售票的安全问题, 可以在一个人进入(函数调用)的时候, 先对临界资源上锁, 修改完之后, 再把锁解开 (排队拿)
func saleTicket(name string) {
for {
mutex.Lock() // 拿到共享资源之前先上锁 !
if ticket > 0 {
time.Sleep(time.Millisecond)
fmt.Println(name, "进来买票了, 剩余票数", ticket)
ticket--
} else {
fmt.Println("票已卖完")
break
}
mutex.Unlock() // 完成操作 就解锁
}
}
但实际上, Go并发编程中流传这样一句经典的话: 不要以共享内存的方式去通信, 而要以通信的方式去共享内存
共享内存方式 -> 锁, 通信方式 -> chan(通道)
同步等待组的使用和介绍
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
wg.Add(2) // WaitGroup为2, 即有2个线程去执行
// wg.Add() 判断还有几个线程去执行, 计数
// wg.Done() 告知线程已经结束 -> 线程-1
// 同步等待组
go test1()
go test2()
fmt.Println("main_start")
wg.Wait() // 等到 wg = 0, 才会继续向下执行
fmt.Println("main_end")
}
func test1() {
for i := 0; i < 10; i++ {
fmt.Println("test1--", i)
}
wg.Done()
}
func test2() {
defer wg.Done() // 这个与test1() 中的 wg.Done() 效果一致
for i := 0; i < 10; i++ {
fmt.Println("test2--", i)
}
}
当然 sync包下有其他锁的使用. 而下一篇笔记是会介绍Go 使用通信的方式(chan 通道)去共享内存~