第九篇文章我们学习了Go解决并发编程问题的三种方式。归根结底都是采用加锁,让并发变成同步访问。那么这些处理方式性能如何呢?

先介绍一个Linux下面统计时间的命令time,具体可以参考:https://www.runoob.com/linux/linux-comm-time.html

这里做一个测试,启动两个协程,一个对数执行++操作,一个对数进行--操作,因为循环的次数是相同的,最后这个数的值还是0。下面看看不同方式的代码,以及编译之后的执行性能。

Channel方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
        "sync"
)

var loopTimes = 2000*10000

func main() {
        num, wg, mux := new(int), new(sync.WaitGroup), make(chan bool, 1)
        defer close(mux)
        wg.Add(2)

        go func(num *int, wg *sync.WaitGroup, mux chan bool) {
                defer wg.Done()
                for i := 1; i <= loopTimes; i++ {
                        mux <- true
                        *num++
                        <-mux
                }
        }(num, wg, mux)

        go func(num *int, wg *sync.WaitGroup, mux chan bool) {
                defer wg.Done()
                for i := 1; i <= loopTimes; i++ {
                        mux <- true
                        *num--
                        <-mux
                }
        }(num, wg, mux)

        wg.Wait()
        println(*num)
}

在Linux服务器中保存上面代码channel.go,命令行编译go build channel.go ,这样在当前目录下就会生成channel可执行文件,用time ./channel运行看看结果:

0

real    0m15.523s
user    0m17.485s
sys     0m0.949s

Mutex方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "sync"

var loopTimes = 2000*10000

func main() {
        num, wg, mux := new(int), new(sync.WaitGroup), new(sync.Mutex)
        wg.Add(2)

        go func(num *int, wg *sync.WaitGroup, mux *sync.Mutex) {
                defer wg.Done()
                for i := 1; i <= loopTimes; i++ {
                        mux.Lock()
                        *num++
                        mux.Unlock()
                }
        }(num, wg, mux)

        go func(num *int, wg *sync.WaitGroup, mux *sync.Mutex) {
                defer wg.Done()
                for i := 1; i <= loopTimes; i++ {
                        mux.Lock()
                        *num--
                        mux.Unlock()
                }
        }(num, wg, mux)

        wg.Wait()
        println(*num)
}

运行结果time ./mutex:

1
2
3
4
5
0

real    0m2.029s
user    0m3.994s
sys     0m0.012s

Atomic方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
        "sync"
        "sync/atomic"
)

var loopTimes = 2000*10000

func main() {
        num, wg := new(int64), new(sync.WaitGroup)
        wg.Add(2)

        go func(num *int64, wg *sync.WaitGroup) {
                defer wg.Done()
                for i := 1; i <= loopTimes; i++ {
                        atomic.AddInt64(num, 1)
                }
        }(num, wg)

        go func(num *int64, wg *sync.WaitGroup) {
                defer wg.Done()
                for i := 1; i <= loopTimes; i++ {
                        atomic.AddInt64(num, -1)
                }
        }(num, wg)

        wg.Wait()
        println(*num)
}

运行结果time ./atomic:

0

real    0m1.328s
user    0m2.605s
sys     0m0.004s

总结

看到上面的运行结果是不是很惊讶?Go语言一大特性,居然性能这么差。那我们还用这个特性干啥呢?

答案是:能不用就不用

请看这篇文章:https://zhuanlan.zhihu.com/p/341931729

或者原文地址:https://linux.cn/article-12984-1.html

在幕后,通道使用锁来序列化访问并提供线程安全性。 因此,通过使用通道同步对内存的访问,你实际上就是在使用锁。 被包装在线程安全队列中的锁。 那么,与仅仅使用标准库 sync 包中的互斥量相比,Go 的花式锁又如何呢? 以下数字是通过使用 Go 的内置基准测试功能,对它们的单个集合连续调用 Put 得出的。

1
2
> BenchmarkSimpleSet-8 			3000000 	391 ns/op
> BenchmarkSimpleChannelSet-8 	1000000 	1699 ns/o

无缓冲通道的情况与此类似,甚至是在争用而不是串行运行的情况下执行相同的测试。

也许 Go 调度器会有所改进,但与此同时,良好的旧互斥量和条件变量是非常好、高效且快速。如果你想要提高性能,请使用久经考验的方法。

网上还有一个有意思的总结,可以参考:

image-20210203001138068

(完)