背景知识:
- 关于线程和进程的关系:简单理解进程是用来管理资源的,而线程是利用cpu执行代码(指令),一个应用程序至少有一个进程,一个进程至少有一个线程。线程是不具备资源的,但是它可以访问所属进程的资源。
- 关于协程和线程的主要区别:线程是抢占式的,任何时刻都有可能被操作系统切换,它是没有控制权的,换句话说一个操作可能执行到一半被中断,cpu去执行其他线程。协程是非抢占式的,由自己主动交出控制权。
goroutine(非抢占式)不是真正意义上的协程,它与传统意义上的协程不同(传统意义上的协程控制权是由自己交出的),而go的调度器会在合适的点进行控制权切换:( 需要注意:只是参考,不能保证一定切换,不能保证其他地方一定不切换 )
1. I/O(例如:fmt包的操作) , select
2. channel
3. 等待锁
4. 函数的调用(这是一个切换的机会,到底切不切换由调度器决定)
5. runtime Gosched()
6. 阻塞,例如time.Sleep()
在传统逻辑中,开发者一般要维护线程池中线程与CPU核心数量间的对应关系。同样go语言也是如此:
runtime.GOMAXPROCS(逻辑CPU数量)
//cpu的逻辑数量如何获取
runtime.NumCPU()
在go语言1.5版本之前使用单线程模式的,但是在1.5版本之后默认会执行:runtime.GOMAXPROCS(runtime.NumCPU())以达到最大程度的利用CPU。接下来对比下面的两个例子:
var num int
//例子一
func main() {
runtime.GOMAXPROCS(1)
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
num ++
wg.Done()
}()
}
wg.Wait()
fmt.Println(num)
}
//例子二
func main() {
runtime.GOMAXPROCS(2)
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
num ++
wg.Done()
}()
}
wg.Wait()
fmt.Println(num)
}
通过运行程序我们可以发现,例子一的结果一直是1000,这是因为“ runtime.GOMAXPROCS(1) ”保证是单线程,所有的goroutine都是在一个线程中串行的,因此不会有访问冲突的问题,但是例子二结果就不一定是1000了,因为开启了两个线程,goroutine就会分布在多个线程中运行,那么就会有访问冲突的问题,解决方案可以使用锁:
func main() {
runtime.GOMAXPROCS(2)
var wg sync.WaitGroup
var lock sync.Mutex
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
lock.Lock()
defer lock.Unlock()
num ++
wg.Done()
}()
}
wg.Wait()
fmt.Println(num)
}
所以goroutine所谓“无锁”的优点只在单线程下有效,如果$GOMAXPROCS > 1并且协程间需要通信,那么就需要引入锁机制来保证数据访问不冲突。