我尝试着用多线程分别运行以下两个函数,并使用 time
来查看它们的 cpu 使用率,一个的使用率就符合启动的线程数(比如 8 线程就得到 800% 的使用率),另一个就无法得到相符合的 cpu 使用率 (比如 8 线程得到 600% 的使用率,2 线程得到 300% 的使用率)。虽然两个函数都没有线程间的互斥锁,这两个函数的区别在于:第一个函数没有对数组的操作,第二个函数有对数组的操作。具体函数如下:
函数 1: addOne
该函数持续做加法运算
func addOne(n int,wg *sync.WaitGroup){
var i int
for i < n{
i++
}
wg.Done()
}
函数 2: appendArray
该函数不断向一个数组中添加元素,为了防止内存溢出,每添加 10,000 个元素,就清空数组。
func appendArray(n int,wg *sync.WaitGroup){
var i int
list := make([]int,10000)
for i < n{
for j := 0 ; j < 10000; j++{
list = append(list, i)
}
list = make([]int,10000)
i++
}
wg.Done()
}
启动多线程的函数: test
该函数将工作总数 totalJobs
平分给每个线程
func test(totalThreads, totalJobs int){
n := totalJobs/totalThreads
var wg sync.WaitGroup
for i := 0; i < totalThreads; i++{
wg.Add(1)
go appendArray(n,&wg)
// go addOne(n,&wg)
}
wg.Wait()
}
通过使用 pprof
, 两个代码的 cpu 分析如下:
函数 1:
函数 2:
我发现对于第二个函数,虽然代码中没有调用锁,在执行的过程中会出现 runtime lock
, runtime futex
之类的锁操作。
这里我不明白的是:
这里总结一下:
这些锁操作的目的是为了 mem alloc
服务的,这包括了 make
和 append
操作。如大家所说,当把 list = make([]int,10000)
变为 list = make([]int, 0, 10000)
后,运行速度是变快了数倍,但是cpu的使用率与之前差异却不大(16线程大概800%)。
如果把函数变成对list中各个元素赋值(代码如下),而不调用 make
,则 cpu 使用率是正常的。可见 make
申请新的空间也会出现锁操作。
func modifyArray(n int,wg *sync.WaitGroup){
list := make([]int,10000)
for i:=0; i < n; i++{
for j := 0 ; j < 10000; j++{
list[j] = i
}
i++
}
wg.Done()
}
1
nifury 2020-01-05 04:02:54 +08:00
因为 append 做不到原子操作吧?
|
2
ncwhale 2020-01-05 04:05:09 +08:00 1
mem alloc 和 move 都是开销大头啊喵……没事别这么玩内存啊喵……碎片化和反复拷贝都是超级消耗资源的,而且,还因为内存 /缓存 /CPU 多核同步等问题,导致必须上锁否则会出现未知内存分布状态喵……
正常一点做法就是别在循环里艹数组尺寸喵!提前预判一下分配空间就好! 否则请使用其它数据结构而不是数组! |
3
mcrwayfun 2020-01-05 07:26:11 +08:00 via iPhone
我想楼主想表达的是切片而不是数组
|
4
gramyang 2020-01-05 07:30:07 +08:00 via Android
我测试过 append 是可以直接操作一个切片声明而不报空指针的,所以 append 里面会调用 make 返回一个切片实例。这种操作必然是耗时操作。
不过题主用的这个 pprof 看起来挺好用的,可以试试 |
5
Herobs 2020-01-05 08:38:41 +08:00 via Android
make 的第二个参数是长度,第三个参数才是容量。
|
6
useben 2020-01-05 09:04:25 +08:00
make 的第二个参数是长度,第三个参数才是容量。+1
你这里还是会指数分配内存 |
8
zhs227 2020-01-05 09:47:04 +08:00 1
append 很多情况下是对对内存进行 malloc, memcpy, free。这些操作未完成之前是不会返回的。所以你会看到各种锁。在有锁的状态下,其它的 goroutine 实际上是在等待。
|
9
dazhangpan 2020-01-05 10:34:14 +08:00
用 time 查看 CPU 的使用率???
|
10
Smash 2020-01-05 13:49:05 +08:00
歪个楼,楼主函数调用图是用什么生成的?
|
12
watsy0007 2020-01-05 14:36:26 +08:00
```golang
func appendArray(n int,wg *sync.WaitGroup){ var i int list := make([]int,10000) for i < n{ for j := 0 ; j < 10000; j++{ list[j] = i } list = make([]int,10000) i++ } wg.Done() } ``` |
13
whoami9894 2020-01-05 18:49:16 +08:00
原因就是#5 说的,应该`make([]int, 0, 10000)`
|
14
CEBBCAT 2020-01-05 19:22:22 +08:00
|
15
vcfghtyjc OP @ncwhale 是否对于任何多线程程序都会出现这个问题(线程申请新的内存时,所有线程需要用锁来同步当前进程控制的内存)?如果用其他语言,如 C++,它的 overhead 还有这么高吗?
|
16
vcfghtyjc OP @dazhangpan 有什么推荐的工具吗?
|
17
fcten 2020-01-06 10:31:56 +08:00
内存分配是必然要加锁的,因为堆内存是整个程序共享的
比较现代的内存分配器会对多线程场景进行优化,把一部分内存划分为线程独占,从而减少锁的使用。 |