Semaphore

现在要完成一个非常简单的任务,给定很多图片的 URL ,将它们下载到本地,你会怎么做?

Sequential

如果你想也不想,就写下了下面这种代码,说明你是一个能干活的程序员,但这并不意味着你是一个优秀的程序员。下面的代码虽然没有什么致命的错误,但是效率非常低,程序运行时 CPU 大部分时间都在等待网络 I/O 而处于空闲状态,导致 CPU 的利用率非常低。

1
2
3
4
5
for idx, task := range tasks {
if err := Do(idx, task); err != nil {
fmt.Println(err)
}
}

Parallel

这种完全由相互独立的子任务组成的任务,被称为 embarrassingly parallel ,这种问题可以通过并行的方法提高程序的性能,而且并行程度越高,性能越好,在程序中我们通过添加 go 这个关键字来创建多个 goroutine ,使多个子任务并发执行,以减少网络 I/O 延迟,提高 CPU 的利用率 。

1
2
3
4
5
6
7
8
9
10
11
var wg sync.WaitGroup
for idx, task := range tasks {
wg.Add(1)
go func(i int, v string) {
defer wg.Done()
if err := Do(i, v); err != nil {
fmt.Println(err)
}
}(idx, task)
}
wg.Wait()

Parallel with Semaphore

但是通过创建多个 goroutine 提高并行度从而提高性能的方法有其局限性,例如一次性创建过多的网络连接,超出了进程打开文件的数量限制时,程序就会报错。所以我们需要限制单位时间内并行程序的个数,通过信号量( semaphore )来限制并行度。在 golang 中,我们可以使用容量为 n 的 buffered channel 来模拟 counting semaphore ,这样以来在每个子任务执行前需要通过 sem <- struct{}{} 来获取执行许可,如果 sem 通道已满,说明当前已经有n个子任务正在执行,该操作就会阻塞等待,直到通道空闲。当任务执行完成后,使用 <-sem 释放执行许可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sem := make(chan struct{}, 8) // 8 jobs at once
var wg sync.WaitGroup
for idx, task := range tasks {
wg.Add(1)
go func(i int, v string) {
defer wg.Done()
sem <- struct{}{}
if err := Do(i, v); err != nil {
fmt.Println(err)
}
<-sem
}(idx, task)
}
wg.Wait()
close(sem)
Pieces of Valuable Programming Knowledges