golang 多协程注意事项

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang 多协程注意事项相关的知识,希望对你有一定的参考价值。

参考技术A 实验100000个goroutines和cpu个数goroutines、单线程进行对比

运行结果:

1.100000个goroutines时间过长,主要原因是因为线程上下文切换有延迟代价。显然100000个goroutines处理这种cpu-bound的工作很不利。io-bound处理可以在io wait的时候去切换别的线程做其他事情,但是对于cpu-bound,它会一直处理work,线程切换会损害性能。
2.cpu数量goroutines时间过长,主要原因是false sharing(cache伪共享),每个core都会去共享变量c的相同cache行,频繁操作c会导致内存抖动(cache和主存直接的换页操作),在golang程序中需要避免因为cache伪共享导致的内存抖动,尽量避免多个线程去频繁操作一个相同变量或者是地址相邻变量。

golang 单协程和多协程的性能测试

测试数据:单协程操作1亿数据,以及多协程(10条协程)操作1亿数据(每条协程操作1kw数据)

废话少说,贴代码:

单协程测试运算:

package main

import (
	"fmt"
	"time"
)

func testNum(num int) {
	for i := 1; i <= 10000000; i++{
		num = num + i
		num = num - i
		num = num * i
		num = num / i
	}
}

func main() {
	start := time.Now()
	for i := 1; i <= 10; i++ {
		testNum(1)
	}
	end :=  time.Now()
	fmt.Println(end.Sub(start).Seconds())
}

运行时间为:0.065330877

多协程测试运算:

package main

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

var synWait sync.WaitGroup

func testNum(num int) {
	for i := 1; i <= 10000000; i++{
		num = num + i
		num = num - i
		num = num * i
		num = num / i
	}
	synWait.Done() 	// 相当于 synWait.Add(-1)
}

func main() {
	start := time.Now()
	for i := 1; i <= 10; i++ {
		synWait.Add(1)
		go testNum(1)
	}
	synWait.Wait()
	end :=  time.Now()
	fmt.Println(end.Sub(start).Seconds())
} 

运行时间为:0.019804929

 

比较结果,和预期的是一样,多协程要比单协程处理数据快,很多人还会去设置runtime.GOMAXPROCS(x),其实

这是远古程序员的做法了,因为go 1.6以上的版本就已经会自动根据计算机核的调用啦!!!

如果没有调用runtime.GOMAXPROCS 去设置CPU,Golang默认使用所有的cpu核

 

以下是以map来做实验,为了测试准确性,统一都加锁

单协程/多协程测试map:

package main

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

var synWait sync.WaitGroup
var normalMap map[int]int
var synMutex sync.Mutex

func testNum(num int) {
	for i := 1; i <= 10000000; i++{
		synMutex.Lock()
		normalMap[i] = num
		synMutex.Unlock()
	}
	synWait.Done() 	// 相当于 synWait.Add(-1)
}

func main() {
	normalMap = make(map[int]int)
	start := time.Now()
	for i := 1; i <= 10; i++ {
		synWait.Add(1)
		testNum(1)		// 单协程操作
		//go testNum(1)		// 多协程并发操作
	}
	synWait.Wait()
	end :=  time.Now()
	fmt.Println(end.Sub(start).Seconds())
}

 

单协程操作 testNum(1), 运行时间为:19.101255922

多协程操作 go testNum(1), 运行时间为:28.210580532

是不是出乎意料!!! 多协程操作map反而慢,这说明map这个数据结构对并发操作效率比较低,如果在保证线性安全的前提下

尽量单协程去操作map,如果上面代码注释掉加锁,单协程操作就更快了, 运行时间为:16.307839364

 

协程通道测试map:

package main

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

var synWait sync.WaitGroup
var normalMap map[int]int

func testNum(data chan int, num int) {
	for i:=1;i<=10000000;i++{
		data <- i
	}
	synWait.Done()
}

func main() {
	normalMap = make(map[int]int)
	data := make(chan int)
	start := time.Now()
	go concurrent(data)
	for i := 1; i <= 10; i++ {
		synWait.Add(1)
		go testNum(data,1)		// 多协程并发操作
	}
	synWait.Wait()
	end :=  time.Now()
	fmt.Println(end.Sub(start).Seconds())
}

func concurrent(data chan int)  {
	for {
		i := <- data
		normalMap[i] = i
	}
}

运行时间为:53.554329275  

通道内部实现也是加锁,这肯定是要比纯用锁慢一点的,这也正好验证了(网上有些人说通道要比加锁快,这是错误的)。但是使用通道是golang的一种哲学意义,规定了入口,里面的数据

结构就不要再担忧,是否要加锁了,因为全部都是安全的(可以避免很多bug,毕竟程序大部分问题还是出自程序员的逻辑代码),还是那句话不要通过共享内存来通信,而要通过通信来共享内存!

 

总结一下吧:(map性能 单协程 > 多协程 > 通道 )

多协程去运算确实快比单协程要快,因为golang会默认根据多核去跑,但是操作map的时候,就要注意,map并发操作效率不及单协程(有点违背多核的感觉)。通道又比纯加锁要慢。

 

以上是关于golang 多协程注意事项的主要内容,如果未能解决你的问题,请参考以下文章

Go中多协程协作之sync.Cond

爬虫小案例:多协程工作

flask多线程多协程操作

一个简单的python多协程实例

玩转pythonpython多协程,多线程的比较

python—多协程爬取斗鱼高颜值美女图片